Skip to main content

maskforge_core/index/
cache.rs

1//! Caches schema-independent byte tries by vocabulary fingerprint.
2
3use std::collections::VecDeque;
4use std::sync::{Arc, Condvar, Mutex, OnceLock};
5
6use rustc_hash::FxHashMap;
7
8use super::slice::TokenSliceCatalog;
9use super::trie::VocabTrie;
10use crate::error::{CompileError, Stage};
11use crate::mem_gate::{acquire_vocab_build_bytes, BuildGate, PermitGuard};
12use crate::vocab::{
13    projected_peak_bytes, try_alloc, CanonicalVocabulary, PreparedVocabulary, Vocabulary,
14};
15
16/// A content address of a vocabulary's token map. Equal fingerprints denote the same token->bytes
17/// relation, so they share a byte-trie.
18#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
19pub struct VocabFingerprint([u8; 32]);
20
21impl VocabFingerprint {
22    /// Computes the fingerprint from the token map SORTED by bytes then id (never the `HashMap`'s
23    /// iteration order) plus the EOS id. Every variable-length field is length-prefixed - the byte
24    pub fn of(vocab: &Vocabulary) -> Result<Self, CompileError> {
25        match PreparedVocabulary::build(vocab) {
26            Ok(prepared) => Ok(Self::of_prepared(&prepared, vocab.eos_token_id())),
27            Err(_) => Self::try_of_unprepared(vocab),
28        }
29    }
30
31    pub(crate) fn of_prepared(prepared: &PreparedVocabulary, eos: u32) -> Self {
32        Self::of_prepared_with_ordinary_max(prepared, eos).0
33    }
34
35    /// Same digest as `of_prepared`, plus the largest non-EOS id seen along the way - one pass
36    /// instead of two when a caller (canonical construction) needs both.
37    pub(crate) fn of_prepared_with_ordinary_max(
38        prepared: &PreparedVocabulary,
39        eos: u32,
40    ) -> (Self, Option<u32>) {
41        let mut hasher = blake3::Hasher::new();
42        let mut ordinary_max = None;
43        for (bytes, ids) in prepared.iter() {
44            hasher.update(&(bytes.len() as u64).to_le_bytes());
45            hasher.update(bytes);
46            hasher.update(&(ids.len() as u64).to_le_bytes());
47            for &id in ids {
48                hasher.update(&id.to_le_bytes());
49                ordinary_max = Some(ordinary_max.map_or(id, |m: u32| m.max(id)));
50            }
51        }
52        hasher.update(&eos.to_le_bytes());
53        (Self(*hasher.finalize().as_bytes()), ordinary_max)
54    }
55
56    /// The same digest as `of_prepared`, computed directly from `vocab` without going through a
57    /// `PreparedVocabulary` (the fallback when preparing one is not possible). Every scratch
58    fn try_of_unprepared(vocab: &Vocabulary) -> Result<Self, CompileError> {
59        let mut entries: Vec<(&[u8], Vec<u32>)> =
60            try_alloc(vocab.tokens().len(), "fingerprint fallback entry scratch")?;
61        for (bytes, ids) in vocab.tokens() {
62            let mut sorted: Vec<u32> = try_alloc(ids.len(), "fingerprint fallback id scratch")?;
63            sorted.extend_from_slice(ids);
64            sorted.sort_unstable();
65            entries.push((bytes.as_slice(), sorted));
66        }
67        entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
68        let mut hasher = blake3::Hasher::new();
69        for (bytes, ids) in &entries {
70            hasher.update(&(bytes.len() as u64).to_le_bytes());
71            hasher.update(bytes);
72            hasher.update(&(ids.len() as u64).to_le_bytes());
73            for &id in ids {
74                hasher.update(&id.to_le_bytes());
75            }
76        }
77        hasher.update(&vocab.eos_token_id().to_le_bytes());
78        Ok(Self(*hasher.finalize().as_bytes()))
79    }
80
81    /// A fixed fingerprint for tests that build tries from synthetic key streams, not a vocabulary.
82    #[cfg(test)]
83    pub(crate) const fn from_bytes(bytes: [u8; 32]) -> Self {
84        Self(bytes)
85    }
86
87    /// The raw 32-byte digest, for exposing the content address outside this crate.
88    #[must_use]
89    pub fn as_bytes(&self) -> [u8; 32] {
90        self.0
91    }
92}
93
94/// Default byte budget for retained tries: `1 << 28` = 256 MiB. Above it, a trie is still built and
95/// returned but not retained, so compiling many distinct vocabularies cannot grow the cache forever.
96const DEFAULT_TRIE_CACHE_BYTES: usize = 1 << 28;
97
98/// Conservative per-entry overhead charged ON TOP of a trie's heap arrays: the `Arc` control block
99/// (two counters) and the boxed `VocabTrie` struct, the `FxHashMap` slot (a 32-byte key plus the
100const CACHE_ENTRY_OVERHEAD: usize = 2 * std::mem::size_of::<usize>()
101    + std::mem::size_of::<VocabTrie>()
102    + std::mem::size_of::<VocabFingerprint>()
103    + std::mem::size_of::<usize>()
104    + 64;
105
106/// Default cap on SIMULTANEOUS distinct-vocabulary trie builds. The retained-byte budget bounds
107/// CACHED tries, not TRANSIENT builds; without this, a burst of distinct large vocabularies (reachable
108const DEFAULT_MAX_CONCURRENT_BUILDS: usize = 8;
109
110/// Default budget for the ESTIMATED transient memory of concurrent trie builds. A count cap alone
111/// does not bound memory (8 huge builds can still exhaust the process), so builds are also admitted
112const DEFAULT_MAX_TRANSIENT_BUILD_BYTES: usize = 1 << 30;
113
114/// A conservative upper bound on the transient bytes one byte-trie build needs, from token count and
115/// total token bytes. The byte sum is a checked fold; overflow saturates to `usize::MAX` (over budget).
116fn estimate_transient_build_bytes(vocab: &Vocabulary) -> usize {
117    let total_bytes = vocab
118        .tokens()
119        .keys()
120        .try_fold(0usize, |acc, k| acc.checked_add(k.len()))
121        .unwrap_or(usize::MAX);
122    let count = vocab.len();
123    total_bytes
124        .saturating_mul(48)
125        .saturating_add(count.saturating_mul(12))
126}
127
128/// The same estimate as `estimate_transient_build_bytes`, read directly off an already-built
129/// `PreparedVocabulary` (its total bytes are already known, so this needs no scan). Uses
130fn estimate_transient_build_bytes_prepared(prepared: &PreparedVocabulary) -> usize {
131    prepared
132        .total_bytes()
133        .saturating_mul(48)
134        .saturating_add(prepared.total_ids().saturating_add(1).saturating_mul(12))
135}
136
137/// Selects how a cache miss builds its trie: from a raw `Vocabulary` (rehashes/resorts) or from an
138/// already-canonical `PreparedVocabulary` (skips both).
139enum BuildSource<'a> {
140    Vocab(&'a Vocabulary),
141    Prepared(&'a PreparedVocabulary),
142}
143
144#[cfg(test)]
145use std::sync::atomic::{AtomicBool, Ordering};
146#[cfg(test)]
147static DELAY_BUILDS: AtomicBool = AtomicBool::new(false);
148#[cfg(test)]
149static DELAY_TEST_LOCK: Mutex<()> = Mutex::new(());
150
151#[cfg(test)]
152thread_local! {
153    static PANIC_ON_BUILD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
154}
155
156/// The one builder's result for a fingerprint, read by every waiter, so a build the budget does NOT
157/// retain is still not rebuilt once per waiter. `CompileError` is `Clone`, so the error is shared too.
158#[derive(Debug)]
159struct BuildSlot {
160    result: Mutex<Option<Result<Arc<VocabTrie>, CompileError>>>,
161    ready: Condvar,
162}
163
164/// A retained trie with its charged byte size and an LRU access stamp.
165#[derive(Debug)]
166struct CacheEntry {
167    trie: Arc<VocabTrie>,
168    bytes: usize,
169    last_used: u64,
170}
171
172#[derive(Debug, Default)]
173struct CacheState {
174    map: FxHashMap<VocabFingerprint, CacheEntry>,
175    bytes: usize,
176    tick: u64,
177    lru: VecDeque<(VocabFingerprint, u64)>,
178    in_flight: FxHashMap<VocabFingerprint, Arc<BuildSlot>>,
179}
180
181impl CacheState {
182    /// Records a touch for LRU: bumps the clock, appends `(fp, tick)` to the log, and returns the
183    /// tick to store as the entry's `last_used`. Compacts the log once it outgrows the live entries.
184    fn touch(&mut self, fp: VocabFingerprint) -> u64 {
185        self.tick += 1;
186        let now = self.tick;
187        self.lru.push_back((fp, now));
188        if self.lru.len() > 4 * self.map.len().max(1) + 64 {
189            self.compact_lru();
190        }
191        now
192    }
193
194    /// Drops every log record that is no longer the live touch for its fingerprint.
195    fn compact_lru(&mut self) {
196        let map = &self.map;
197        self.lru
198            .retain(|(fp, tick)| map.get(fp).is_some_and(|e| e.last_used == *tick));
199    }
200
201    /// Evicts least-recently-used retained entries until `need` more bytes fit under `budget`. Only
202    /// the cache's `Arc` is dropped; an artifact still holding its own `Arc` stays valid.
203    fn evict_until_fits(&mut self, need: usize, budget: usize, stats: &mut CacheStats) {
204        while self.bytes.saturating_add(need) > budget {
205            let Some((fp, tick)) = self.lru.pop_front() else {
206                break; // log exhausted; map empty (the new entry alone exceeds the budget: oversized)
207            };
208            if self.map.get(&fp).is_some_and(|e| e.last_used == tick) {
209                if let Some(e) = self.map.remove(&fp) {
210                    self.bytes -= e.bytes;
211                    stats.evictions += 1;
212                }
213            } // else: a stale record from an earlier touch that was since superseded - skip it
214        }
215    }
216}
217
218/// Cumulative cache-traffic counters, since creation (not reset by `clear`).
219#[derive(Debug, Default)]
220struct CacheStats {
221    hits: u64,
222    misses: u64,
223    evictions: u64,
224    oversized_bypasses: u64,
225}
226
227/// A thread-safe, byte-bounded cache of byte-tries keyed by vocabulary fingerprint. Read-mostly
228/// after warm-up; once the budget is reached, further tries are built and returned but not retained.
229#[derive(Debug)]
230pub struct TrieCache {
231    state: Mutex<CacheState>,
232    stats: Mutex<CacheStats>,
233    budget: usize,
234    gate: BuildGate,
235}
236
237impl Default for TrieCache {
238    fn default() -> Self {
239        Self::with_build_limit(DEFAULT_TRIE_CACHE_BYTES, DEFAULT_MAX_CONCURRENT_BUILDS)
240    }
241}
242
243/// Publishes an error to the build slot and clears the in-flight mark on drop UNLESS disarmed, so a
244/// panicking builder never leaves waiters hung on a slot that will never be filled, nor a permanently
245struct SlotGuard<'a> {
246    cache: &'a TrieCache,
247    fp: VocabFingerprint,
248    slot: Arc<BuildSlot>,
249    armed: bool,
250}
251
252impl Drop for SlotGuard<'_> {
253    fn drop(&mut self) {
254        if self.armed {
255            let mut r = self.slot.result.lock().unwrap_or_else(|p| p.into_inner());
256            if r.is_none() {
257                *r = Some(Err(CompileError::new(
258                    crate::error::ErrorCode::InternalLimitExceeded,
259                    crate::error::Stage::L4Bind,
260                    "trie build failed to publish a result",
261                )));
262            }
263            drop(r);
264            self.slot.ready.notify_all();
265            self.cache
266                .state
267                .lock()
268                .unwrap_or_else(|p| p.into_inner())
269                .in_flight
270                .remove(&self.fp);
271        }
272    }
273}
274
275impl TrieCache {
276    /// An empty cache with the default byte budget and concurrent-build cap.
277    #[must_use]
278    pub fn new() -> Self {
279        Self::default()
280    }
281
282    /// An empty cache with an explicit retained-byte budget.
283    #[must_use]
284    pub fn with_budget(budget: usize) -> Self {
285        Self::with_build_limit(budget, DEFAULT_MAX_CONCURRENT_BUILDS)
286    }
287
288    /// An empty cache with an explicit retained-byte budget and the default build gate.
289    #[must_use]
290    pub fn with_build_limit(budget: usize, max_concurrent_builds: usize) -> Self {
291        Self::with_build_gate(
292            budget,
293            max_concurrent_builds,
294            DEFAULT_MAX_TRANSIENT_BUILD_BYTES,
295        )
296    }
297
298    /// An empty cache with an explicit retained-byte budget, a concurrent-build count cap, AND a
299    /// transient-build byte budget (so many large concurrent builds cannot exhaust memory).
300    #[must_use]
301    pub fn with_build_gate(
302        budget: usize,
303        max_concurrent_builds: usize,
304        max_transient_build_bytes: usize,
305    ) -> Self {
306        Self {
307            state: Mutex::new(CacheState::default()),
308            stats: Mutex::new(CacheStats::default()),
309            budget,
310            gate: BuildGate::new(
311                max_concurrent_builds,
312                max_transient_build_bytes,
313                Stage::L4Bind,
314            ),
315        }
316    }
317
318    /// The peak number of simultaneous distinct-vocabulary builds observed (for tests/telemetry).
319    #[must_use]
320    pub fn peak_concurrent_builds(&self) -> usize {
321        self.gate.peak_concurrent()
322    }
323
324    /// The peak concurrent transient-build bytes charged (for tests/telemetry).
325    #[must_use]
326    pub fn peak_concurrent_build_bytes(&self) -> usize {
327        self.gate.peak_bytes()
328    }
329
330    /// The cached byte-trie for `vocab`, building and inserting it on a miss. Two vocabularies with
331    /// equal fingerprints get the same `Arc`; the trie is built outside the lock, so a rare double
332    pub fn get_or_build_byte(&self, vocab: &Vocabulary) -> Result<Arc<VocabTrie>, CompileError> {
333        self.get_or_build_byte_prevalidated(vocab, VocabFingerprint::of(vocab)?)
334    }
335
336    /// Same lookup as `get_or_build_byte` but skips rehashing `vocab` on a hit too. Not exposed
337    /// publicly: a mismatched `(vocab, fingerprint)` pair here is never re-verified against
338    pub(crate) fn get_or_build_byte_prevalidated(
339        &self,
340        vocab: &Vocabulary,
341        fingerprint: VocabFingerprint,
342    ) -> Result<Arc<VocabTrie>, CompileError> {
343        self.get_or_build_byte_impl(BuildSource::Vocab(vocab), fingerprint)
344    }
345
346    /// Same lookup, building from an ALREADY-canonical `PreparedVocabulary` on a miss - the entry
347    /// point `bind` uses, so a `VocabularyHandle`'s one sort is never repeated at first-bind time.
348    pub(crate) fn get_or_build_byte_from_prepared(
349        &self,
350        prepared: &PreparedVocabulary,
351        fingerprint: VocabFingerprint,
352    ) -> Result<Arc<VocabTrie>, CompileError> {
353        self.get_or_build_byte_impl(BuildSource::Prepared(prepared), fingerprint)
354    }
355
356    /// The shared cache/single-flight/build-gate logic; `source` only decides HOW a miss builds.
357    fn get_or_build_byte_impl(
358        &self,
359        source: BuildSource<'_>,
360        fingerprint: VocabFingerprint,
361    ) -> Result<Arc<VocabTrie>, CompileError> {
362        let slot = {
363            let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
364            if state.map.contains_key(&fingerprint) {
365                let now = state.touch(fingerprint);
366                let entry = state
367                    .map
368                    .get_mut(&fingerprint)
369                    .expect("just confirmed present under the same lock hold");
370                entry.last_used = now;
371                let trie = entry.trie.clone();
372                self.stats.lock().unwrap_or_else(|p| p.into_inner()).hits += 1;
373                return Ok(trie);
374            }
375            if let Some(slot) = state.in_flight.get(&fingerprint).cloned() {
376                drop(state);
377                let result = {
378                    let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
379                    while r.is_none() {
380                        r = slot.ready.wait(r).unwrap_or_else(|p| p.into_inner());
381                    }
382                    r.clone().expect("slot is ready")
383                };
384                if result.is_ok() {
385                    self.stats.lock().unwrap_or_else(|p| p.into_inner()).hits += 1;
386                }
387                return result;
388            }
389            let slot = Arc::new(BuildSlot {
390                result: Mutex::new(None),
391                ready: Condvar::new(),
392            });
393            state.in_flight.insert(fingerprint, slot.clone());
394            slot
395        };
396
397        let mut guard = SlotGuard {
398            cache: self,
399            fp: fingerprint,
400            slot: slot.clone(),
401            armed: true,
402        };
403        let built: Result<Arc<VocabTrie>, CompileError> = (|| {
404            let est = match source {
405                BuildSource::Vocab(v) => estimate_transient_build_bytes(v),
406                BuildSource::Prepared(p) => estimate_transient_build_bytes_prepared(p),
407            };
408            let _permit = self.gate.acquire(est)?;
409            #[cfg(test)]
410            if DELAY_BUILDS.load(Ordering::Relaxed) {
411                std::thread::sleep(std::time::Duration::from_millis(20));
412            }
413            #[cfg(test)]
414            if PANIC_ON_BUILD.with(|p| p.get()) {
415                panic!("injected test panic mid-build");
416            }
417            let trie = match source {
418                BuildSource::Vocab(v) => VocabTrie::build_byte_with_fingerprint(v, fingerprint)?,
419                BuildSource::Prepared(p) => VocabTrie::build_byte_from_prepared(p, fingerprint)?,
420            };
421            Ok(Arc::new(trie))
422        })();
423
424        {
425            let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
426            *r = Some(built.clone());
427            slot.ready.notify_all();
428        }
429        {
430            let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
431            state.in_flight.remove(&fingerprint);
432            if let Ok(ref arc) = built {
433                let mut stats = self.stats.lock().unwrap_or_else(|p| p.into_inner());
434                stats.misses += 1;
435                let entry_bytes = arc.heap_bytes().saturating_add(CACHE_ENTRY_OVERHEAD);
436                if entry_bytes > self.budget {
437                    stats.oversized_bypasses += 1; // one trie bigger than the whole budget: never cache
438                } else {
439                    state.evict_until_fits(entry_bytes, self.budget, &mut stats);
440                    let now = state.touch(fingerprint);
441                    state.bytes += entry_bytes;
442                    state.map.insert(
443                        fingerprint,
444                        CacheEntry {
445                            trie: arc.clone(),
446                            bytes: entry_bytes,
447                            last_used: now,
448                        },
449                    );
450                }
451            }
452        }
453        guard.armed = false; // published + cleared cleanly; the guard has nothing to repair
454        built
455    }
456
457    /// The byte-trie for `handle`'s vocabulary, wrapped as an opaque `BoundByteTrie` whose
458    /// fingerprint is guaranteed (by construction, not by caller honesty) to be `handle`'s own -
459    pub fn bind(&self, handle: &VocabularyHandle) -> Result<BoundByteTrie, CompileError> {
460        let trie = self.get_or_build_byte_from_prepared(handle.prepared(), handle.fingerprint)?;
461        Ok(BoundByteTrie {
462            trie,
463            fingerprint: handle.fingerprint,
464        })
465    }
466
467    /// Drops every retained trie. Also clears the LRU touch-log, so repeated fill/clear cycles
468    /// cannot accumulate stale log records indefinitely. Traffic counters (`stats()`) are NOT reset -
469    pub fn clear(&self) {
470        let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
471        state.map.clear();
472        state.bytes = 0;
473        state.lru.clear();
474        state.tick = 0;
475    }
476
477    /// The number of retained tries.
478    #[must_use]
479    pub fn len(&self) -> usize {
480        self.state
481            .lock()
482            .unwrap_or_else(|p| p.into_inner())
483            .map
484            .len()
485    }
486
487    /// Whether the cache retains no trie.
488    #[must_use]
489    pub fn is_empty(&self) -> bool {
490        self.len() == 0
491    }
492
493    /// Retained trie bytes. Bounds the CACHE only - a trie an active `PyIndex`/`CompiledArtifact`
494    /// holds its own `Arc` to stays alive via that reference even if evicted or never cached here.
495    #[must_use]
496    pub fn bytes(&self) -> usize {
497        self.state.lock().unwrap_or_else(|p| p.into_inner()).bytes
498    }
499
500    /// `(hits, misses, evictions, oversized_bypasses)` since creation - a lifetime traffic total,
501    /// NOT reset by `clear()` (which only drops currently-retained content). Single-flight caps
502    #[must_use]
503    pub fn stats(&self) -> (u64, u64, u64, u64) {
504        let s = self.stats.lock().unwrap_or_else(|p| p.into_inner());
505        (s.hits, s.misses, s.evictions, s.oversized_bypasses)
506    }
507}
508
509/// An immutable, opaque handle owning ONE canonical vocabulary representation and its content
510/// fingerprint - always computed here, never caller-suppliable.
511#[derive(Clone, Debug)]
512pub struct VocabularyHandle {
513    canonical: Arc<CanonicalVocabulary>,
514    fingerprint: VocabFingerprint,
515    token_slices: Arc<OnceLock<Option<Arc<TokenSliceCatalog>>>>,
516}
517
518/// One `(&[u8], &[u32])` sort-scratch entry: two fat pointers, structurally fatter than the packed
519/// path's one `u32` sort index per token, so it is its own term, not folded into that estimate.
520const PAIR_SCRATCH_BYTES_PER_TOKEN: usize = 4 * std::mem::size_of::<usize>();
521
522/// One `(Vec<u8>, Vec<u32>)` outer-descriptor entry: two 3-word `Vec` headers (ptr/len/cap each).
523const TUPLE_DESCRIPTOR_BYTES_PER_TOKEN: usize = 2 * 3 * std::mem::size_of::<usize>();
524
525/// Per-token `FxHashMap<Vec<u8>, Vec<u32>>` slot cost: the key+value payload plus hashbrown
526/// control/load-factor slack. An over-estimate, not an exact bound on the opaque allocation.
527const HASHMAP_SLOT_BYTES_PER_TOKEN: usize = std::mem::size_of::<(Vec<u8>, Vec<u32>)>() + 16;
528
529/// A conservative estimated peak (not a structural bound - hashbrown's real allocation is opaque)
530/// for `VocabularyHandle::new`'s tuple/`HashMap` construction: max of copy/map-build/canonical-build.
531#[must_use]
532pub(crate) fn handle_construction_peak_bytes_from_totals(
533    total_bytes: usize,
534    total_ids: usize,
535    token_count: usize,
536) -> usize {
537    let ids_bytes = total_ids.saturating_mul(4);
538    let descriptors = token_count.saturating_mul(TUPLE_DESCRIPTOR_BYTES_PER_TOKEN);
539    let hashmap_buckets = token_count.saturating_mul(HASHMAP_SLOT_BYTES_PER_TOKEN);
540
541    let copy_peak = descriptors
542        .saturating_add(total_bytes)
543        .saturating_add(ids_bytes);
544    let map_peak = copy_peak.saturating_add(hashmap_buckets);
545
546    let source_map_retained = total_bytes
547        .saturating_add(ids_bytes)
548        .saturating_add(hashmap_buckets);
549    let canonical_build = projected_peak_bytes(total_bytes, total_ids, token_count)
550        .saturating_add(token_count.saturating_mul(PAIR_SCRATCH_BYTES_PER_TOKEN));
551    let canonical_peak = source_map_retained.saturating_add(canonical_build);
552
553    copy_peak.max(map_peak).max(canonical_peak)
554}
555
556/// `handle_construction_peak_bytes_from_totals`, from an already-built `vocab`'s own totals.
557fn handle_construction_peak_bytes(vocab: &Vocabulary) -> usize {
558    let total_bytes = vocab
559        .tokens()
560        .keys()
561        .try_fold(0usize, |acc, k| acc.checked_add(k.len()))
562        .unwrap_or(usize::MAX);
563    let total_ids = vocab
564        .tokens()
565        .values()
566        .try_fold(0usize, |acc, v| acc.checked_add(v.len()))
567        .unwrap_or(usize::MAX);
568    handle_construction_peak_bytes_from_totals(total_bytes, total_ids, vocab.tokens().len())
569}
570
571impl VocabularyHandle {
572    /// Wraps `vocab`, building the canonical representation once, admitted against the shared
573    /// build-memory gate so concurrent large constructions cannot each exceed the process budget.
574    pub fn new(vocab: Arc<Vocabulary>) -> Result<Self, crate::error::CompileError> {
575        Self::new_with_logits_vocab_size(vocab, None)
576    }
577
578    /// `new`, with an explicit mask width instead of the inferred one. Rejected if `Some` and
579    /// smaller than the largest token/EOS id actually present.
580    pub fn new_with_logits_vocab_size(
581        vocab: Arc<Vocabulary>,
582        logits_vocab_size: Option<usize>,
583    ) -> Result<Self, crate::error::CompileError> {
584        let permit = acquire_vocab_build_bytes(handle_construction_peak_bytes(&vocab))?;
585        Self::new_admitted_with_logits_vocab_size(vocab, logits_vocab_size, &permit)
586    }
587
588    /// `new`, but the caller already holds a `PermitGuard` for this whole operation - re-verified
589    /// against this route's own estimate first, so an undersized permit is rejected, not trusted.
590    pub fn new_admitted(
591        vocab: Arc<Vocabulary>,
592        permit: &PermitGuard<'_>,
593    ) -> Result<Self, crate::error::CompileError> {
594        Self::new_admitted_with_logits_vocab_size(vocab, None, permit)
595    }
596
597    /// `new_admitted`, with the same explicit-mask-width contract as `new_with_logits_vocab_size`.
598    pub fn new_admitted_with_logits_vocab_size(
599        vocab: Arc<Vocabulary>,
600        logits_vocab_size: Option<usize>,
601        permit: &PermitGuard<'_>,
602    ) -> Result<Self, crate::error::CompileError> {
603        permit.ensure_covers(handle_construction_peak_bytes(&vocab))?;
604        let canonical = Arc::new(CanonicalVocabulary::from_vocabulary_with_logits_vocab_size(
605            &vocab,
606            logits_vocab_size,
607        )?);
608        let fingerprint = canonical.fingerprint();
609        Ok(Self {
610            canonical,
611            fingerprint,
612            token_slices: Arc::new(OnceLock::new()),
613        })
614    }
615
616    /// Builds directly from four packed CSR buffers (the tuple-free fast path): no intermediate
617    /// `Vocabulary`/`HashMap`. Same validation as `new`, admitted before any construction runs.
618    pub fn from_packed(
619        token_bytes: &[u8],
620        byte_offsets: &[u32],
621        token_ids: &[u32],
622        id_offsets: &[u32],
623        eos_token_id: u32,
624    ) -> Result<Self, crate::error::CompileError> {
625        Self::from_packed_with_logits_vocab_size(
626            token_bytes,
627            byte_offsets,
628            token_ids,
629            id_offsets,
630            eos_token_id,
631            None,
632        )
633    }
634
635    /// `from_packed`, with the same explicit-mask-width contract as `new_with_logits_vocab_size`.
636    #[allow(clippy::too_many_arguments)]
637    pub fn from_packed_with_logits_vocab_size(
638        token_bytes: &[u8],
639        byte_offsets: &[u32],
640        token_ids: &[u32],
641        id_offsets: &[u32],
642        eos_token_id: u32,
643        logits_vocab_size: Option<usize>,
644    ) -> Result<Self, crate::error::CompileError> {
645        let token_count = byte_offsets.len().saturating_sub(1);
646        let peak = projected_peak_bytes(token_bytes.len(), token_ids.len(), token_count);
647        let permit = acquire_vocab_build_bytes(peak)?;
648        Self::from_packed_admitted_with_logits_vocab_size(
649            token_bytes,
650            byte_offsets,
651            token_ids,
652            id_offsets,
653            eos_token_id,
654            logits_vocab_size,
655            &permit,
656        )
657    }
658
659    /// `from_packed`, but the caller already holds a `PermitGuard` covering the whole ingestion -
660    /// re-verified the same way as `new_admitted`, from the actual slices.
661    pub fn from_packed_admitted(
662        token_bytes: &[u8],
663        byte_offsets: &[u32],
664        token_ids: &[u32],
665        id_offsets: &[u32],
666        eos_token_id: u32,
667        permit: &PermitGuard<'_>,
668    ) -> Result<Self, crate::error::CompileError> {
669        Self::from_packed_admitted_with_logits_vocab_size(
670            token_bytes,
671            byte_offsets,
672            token_ids,
673            id_offsets,
674            eos_token_id,
675            None,
676            permit,
677        )
678    }
679
680    /// `from_packed_admitted`, with the same explicit-mask-width contract as
681    /// `new_with_logits_vocab_size`.
682    #[allow(clippy::too_many_arguments)]
683    pub fn from_packed_admitted_with_logits_vocab_size(
684        token_bytes: &[u8],
685        byte_offsets: &[u32],
686        token_ids: &[u32],
687        id_offsets: &[u32],
688        eos_token_id: u32,
689        logits_vocab_size: Option<usize>,
690        permit: &PermitGuard<'_>,
691    ) -> Result<Self, crate::error::CompileError> {
692        let token_count = byte_offsets.len().saturating_sub(1);
693        let required = projected_peak_bytes(token_bytes.len(), token_ids.len(), token_count);
694        permit.ensure_covers(required)?;
695        let canonical = Arc::new(CanonicalVocabulary::from_packed_with_logits_vocab_size(
696            token_bytes,
697            byte_offsets,
698            token_ids,
699            id_offsets,
700            eos_token_id,
701            logits_vocab_size,
702        )?);
703        let fingerprint = canonical.fingerprint();
704        Ok(Self {
705            canonical,
706            fingerprint,
707            token_slices: Arc::new(OnceLock::new()),
708        })
709    }
710
711    /// Builds directly from a DENSE, ID-ORDERED tokenizer decode: `decoded[i]` (via `byte_offsets`)
712    /// is token id `i`'s bytes, `present[i]` says whether slot `i` exists. No id list, no
713    pub fn from_dense_id_ordered(
714        token_bytes: &[u8],
715        byte_offsets: &[u32],
716        present: &[bool],
717        eos_token_id: u32,
718    ) -> Result<Self, crate::error::CompileError> {
719        Self::from_dense_id_ordered_with_logits_vocab_size(
720            token_bytes,
721            byte_offsets,
722            present,
723            eos_token_id,
724            None,
725        )
726    }
727
728    /// `from_dense_id_ordered`, with the same explicit-mask-width contract as
729    /// `new_with_logits_vocab_size`.
730    pub fn from_dense_id_ordered_with_logits_vocab_size(
731        token_bytes: &[u8],
732        byte_offsets: &[u32],
733        present: &[bool],
734        eos_token_id: u32,
735        logits_vocab_size: Option<usize>,
736    ) -> Result<Self, crate::error::CompileError> {
737        let present_count = present.iter().filter(|&&p| p).count();
738        let peak = projected_peak_bytes(token_bytes.len(), present_count, present_count);
739        let permit = acquire_vocab_build_bytes(peak)?;
740        Self::from_dense_id_ordered_admitted_with_logits_vocab_size(
741            token_bytes,
742            byte_offsets,
743            present,
744            eos_token_id,
745            logits_vocab_size,
746            &permit,
747        )
748    }
749
750    /// `from_dense_id_ordered`, but the caller already holds a `PermitGuard` covering the whole
751    /// ingestion - re-verified from the actual slices, same as `from_packed_admitted`.
752    pub fn from_dense_id_ordered_admitted(
753        token_bytes: &[u8],
754        byte_offsets: &[u32],
755        present: &[bool],
756        eos_token_id: u32,
757        permit: &PermitGuard<'_>,
758    ) -> Result<Self, crate::error::CompileError> {
759        Self::from_dense_id_ordered_admitted_with_logits_vocab_size(
760            token_bytes,
761            byte_offsets,
762            present,
763            eos_token_id,
764            None,
765            permit,
766        )
767    }
768
769    /// `from_dense_id_ordered_admitted`, with the same explicit-mask-width contract as
770    /// `new_with_logits_vocab_size`.
771    pub fn from_dense_id_ordered_admitted_with_logits_vocab_size(
772        token_bytes: &[u8],
773        byte_offsets: &[u32],
774        present: &[bool],
775        eos_token_id: u32,
776        logits_vocab_size: Option<usize>,
777        permit: &PermitGuard<'_>,
778    ) -> Result<Self, crate::error::CompileError> {
779        let present_count = present.iter().filter(|&&p| p).count();
780        let required = projected_peak_bytes(token_bytes.len(), present_count, present_count);
781        permit.ensure_covers(required)?;
782        let canonical = Arc::new(
783            CanonicalVocabulary::from_dense_id_ordered_with_logits_vocab_size(
784                token_bytes,
785                byte_offsets,
786                present,
787                eos_token_id,
788                logits_vocab_size,
789            )?,
790        );
791        let fingerprint = canonical.fingerprint();
792        Ok(Self {
793            canonical,
794            fingerprint,
795            token_slices: Arc::new(OnceLock::new()),
796        })
797    }
798
799    /// The one canonical vocabulary representation.
800    #[must_use]
801    pub(crate) fn canonical(&self) -> &Arc<CanonicalVocabulary> {
802        &self.canonical
803    }
804
805    /// The vocabulary's content fingerprint, computed once at construction.
806    #[must_use]
807    pub fn fingerprint(&self) -> VocabFingerprint {
808        self.fingerprint
809    }
810
811    /// The mask width (`max(largest token id, EOS id) + 1`, or the explicit `logits_vocab_size`
812    /// this handle was built with) - the number of bits one mask row covers.
813    #[must_use]
814    pub fn mask_vocab_size(&self) -> usize {
815        self.canonical.mask_vocab_size()
816    }
817
818    /// The EOS token id (a side value, never a mask bit).
819    #[must_use]
820    pub fn eos_token_id(&self) -> u32 {
821        self.canonical.eos_token_id()
822    }
823
824    /// The number of stored ordinary token ids plus one for EOS (NOT the distinct byte-string
825    /// count and NOT the mask width).
826    #[must_use]
827    pub fn len(&self) -> usize {
828        self.canonical.len()
829    }
830
831    /// True when the token map has no records (matches `Vocabulary::is_empty()`).
832    #[must_use]
833    pub fn is_empty(&self) -> bool {
834        self.canonical.is_empty()
835    }
836
837    /// `id`'s byte string, or `None` if `id` names no ordinary token (including EOS).
838    #[must_use]
839    pub fn token_bytes(&self, id: u32) -> Option<&[u8]> {
840        self.canonical.token_bytes(id)
841    }
842
843    /// Every record as `(bytes, ids)`, byte-sorted (for a caller with no precomputed mask table).
844    pub fn iter_records(&self) -> impl Iterator<Item = (&[u8], &[u32])> {
845        self.canonical.iter()
846    }
847
848    /// The canonical form's shared byte-sorted view, reused by `TrieCache::bind` so a first trie
849    /// build never re-sorts the token map fingerprinting already sorted.
850    pub(crate) fn prepared(&self) -> &PreparedVocabulary {
851        self.canonical.prepared()
852    }
853
854    pub(crate) fn token_slices(&self) -> Option<&Arc<TokenSliceCatalog>> {
855        self.token_slices
856            .get_or_init(|| TokenSliceCatalog::try_build(&self.canonical).ok().flatten())
857            .as_ref()
858    }
859
860    /// Bytes retained by the optional vocabulary-derived token-slice catalogue.
861    #[must_use]
862    pub fn token_slice_retained_bytes(&self) -> usize {
863        self.token_slices()
864            .map_or(0, |catalog| catalog.retained_bytes())
865    }
866}
867
868/// A byte-trie proven paired with a specific `VocabularyHandle`. Private fields: the only public
869/// constructor is `TrieCache::bind`, which always derives the fingerprint from the SAME handle the
870#[derive(Clone, Debug)]
871pub struct BoundByteTrie {
872    trie: Arc<VocabTrie>,
873    fingerprint: VocabFingerprint,
874}
875
876impl BoundByteTrie {
877    pub(crate) fn trie(&self) -> &VocabTrie {
878        &self.trie
879    }
880
881    /// The shared trie `Arc`, for the lazy bind path to retain without rebuilding.
882    pub(crate) fn trie_arc(&self) -> Arc<VocabTrie> {
883        self.trie.clone()
884    }
885
886    pub(crate) fn fingerprint(&self) -> VocabFingerprint {
887        self.fingerprint
888    }
889}
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894    use crate::error::ErrorCode;
895    use crate::vocab::build_vocabulary;
896    use rustc_hash::FxHashMap as Map;
897
898    fn vocab(pairs: &[(&[u8], u32)], eos: u32) -> Vocabulary {
899        let mut map: Map<Vec<u8>, Vec<u32>> = Map::default();
900        for &(bytes, id) in pairs {
901            map.insert(bytes.to_vec(), vec![id]);
902        }
903        build_vocabulary(eos, map).expect("vocab")
904    }
905
906    #[test]
907    fn fingerprint_is_injective_over_marker_byte_id_lists() {
908        let fp = |pairs: &[(&[u8], &[u32])], eos: u32| {
909            let mut m: Map<Vec<u8>, Vec<u32>> = Map::default();
910            for &(b, ids) in pairs {
911                m.insert(b.to_vec(), ids.to_vec());
912            }
913            VocabFingerprint::of(&build_vocabulary(eos, m).expect("vocab")).expect("fingerprint")
914        };
915        let one_token = fp(&[(b"a", &[0x0000_00fe, 0x0000_00ff])], 9);
916        let two_tokens = fp(&[(b"a", &[0x0000_00fe]), (b"b", &[0x0000_00ff])], 9);
917        assert_ne!(
918            one_token, two_tokens,
919            "id-list grouping must change the fingerprint"
920        );
921        let reordered = fp(&[(b"a", &[0x0000_00ff, 0x0000_00fe])], 9);
922        assert_eq!(
923            one_token, reordered,
924            "id order within a token must not matter"
925        );
926        assert_ne!(
927            one_token,
928            fp(&[(b"a", &[0x0000_00fe, 0x0000_00ff])], 10),
929            "eos must change the fingerprint"
930        );
931    }
932
933    #[test]
934    fn same_vocabulary_returns_the_same_arc() {
935        let cache = TrieCache::new();
936        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
937        let first = cache.get_or_build_byte(&v).unwrap();
938        let second = cache.get_or_build_byte(&v).unwrap();
939        assert!(
940            Arc::ptr_eq(&first, &second),
941            "a cache hit must reuse the Arc"
942        );
943    }
944
945    #[test]
946    fn prevalidated_lookup_agrees_with_the_generic_one() {
947        let cache = TrieCache::new();
948        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
949        let generic = cache.get_or_build_byte(&v).unwrap();
950        let fp = VocabFingerprint::of(&v).unwrap();
951        let fast = cache.get_or_build_byte_prevalidated(&v, fp).unwrap();
952        assert!(
953            Arc::ptr_eq(&generic, &fast),
954            "same vocabulary, same cached Arc"
955        );
956    }
957
958    #[test]
959    fn a_handles_fingerprint_matches_the_direct_unprepared_computation() {
960        let cases = [
961            vocab(&[(b"a", 0), (b"b", 1)], 9),
962            vocab(&[(b"cat", 17), (b"car", 42)], 1000),
963            vocab(&[(&[0xff, 0xfe], 3), (&[0x00], 4)], 9999),
964        ];
965        for v in cases {
966            let direct = VocabFingerprint::of(&v).unwrap();
967            let via_handle = VocabularyHandle::new(Arc::new(v)).unwrap().fingerprint();
968            assert_eq!(
969                direct, via_handle,
970                "handle fingerprint must match the direct one"
971            );
972        }
973    }
974
975    #[test]
976    fn dense_id_ordered_handle_matches_the_tuple_handle_on_equivalent_content() {
977        let dense_v = vocab(&[(b"cat", 0), (b"car", 1), (b"c", 3)], 9999);
978        let via_new = VocabularyHandle::new(Arc::new(dense_v)).unwrap();
979
980        let token_bytes = b"catcarc";
981        let byte_offsets = [0u32, 3, 6, 6, 7]; // cat, car, (absent), c
982        let present = [true, true, false, true];
983        let via_dense =
984            VocabularyHandle::from_dense_id_ordered(token_bytes, &byte_offsets, &present, 9999)
985                .unwrap();
986
987        assert_eq!(via_new.fingerprint(), via_dense.fingerprint());
988        assert_eq!(via_new.mask_vocab_size(), via_dense.mask_vocab_size());
989    }
990
991    #[test]
992    fn dense_id_ordered_handle_rejects_eos_present_as_an_ordinary_slot() {
993        let token_bytes = b"ab";
994        let byte_offsets = [0u32, 1, 2];
995        let present = [true, true];
996        let err = VocabularyHandle::from_dense_id_ordered(token_bytes, &byte_offsets, &present, 1)
997            .expect_err("eos id 1 collides with the present 'b' slot");
998        assert_eq!(err.code, ErrorCode::MalformedTokenizer);
999    }
1000
1001    #[test]
1002    fn a_bind_hit_does_not_rebuild_and_only_the_miss_counts() {
1003        let cache = TrieCache::new();
1004        let handle = VocabularyHandle::new(Arc::new(vocab(&[(b"a", 0), (b"b", 1)], 9))).unwrap();
1005        let (h0, m0, _, _) = cache.stats();
1006        let _first = cache.bind(&handle).unwrap(); // miss: builds once
1007        let (_h1, m1, _, _) = cache.stats();
1008        let _second = cache.bind(&handle).unwrap(); // hit: no rebuild, no rehash
1009        let (h2, m2, _, _) = cache.stats();
1010        assert_eq!(m1, m0 + 1, "the first bind is the only miss");
1011        assert_eq!(m2, m1, "a hit must not add a miss");
1012        assert_eq!(h2, h0 + 1, "the second bind is a hit");
1013        assert_eq!(cache.len(), 1, "one trie retained for one vocabulary");
1014    }
1015
1016    #[test]
1017    fn different_vocabularies_are_different_keys() {
1018        let cache = TrieCache::new();
1019        let a = vocab(&[(b"a", 0)], 9);
1020        let b = vocab(&[(b"b", 0)], 9);
1021        let ta = cache.get_or_build_byte(&a).unwrap();
1022        let tb = cache.get_or_build_byte(&b).unwrap();
1023        assert!(
1024            !Arc::ptr_eq(&ta, &tb),
1025            "distinct token maps must not share a trie"
1026        );
1027        assert_ne!(
1028            VocabFingerprint::of(&a).unwrap(),
1029            VocabFingerprint::of(&b).unwrap()
1030        );
1031    }
1032
1033    #[test]
1034    fn fingerprint_ignores_hashmap_order_but_tracks_eos() {
1035        let a = vocab(&[(b"a", 0), (b"b", 1)], 9);
1036        let b = vocab(&[(b"b", 1), (b"a", 0)], 9);
1037        assert_eq!(
1038            VocabFingerprint::of(&a).unwrap(),
1039            VocabFingerprint::of(&b).unwrap()
1040        );
1041        let eos_changed = vocab(&[(b"a", 0), (b"b", 1)], 8);
1042        assert_ne!(
1043            VocabFingerprint::of(&a).unwrap(),
1044            VocabFingerprint::of(&eos_changed).unwrap()
1045        );
1046    }
1047
1048    #[test]
1049    fn boundary_ambiguity_is_separated_by_domain_markers() {
1050        let one = vocab(&[(b"ab", 0)], 9);
1051        let two = vocab(&[(b"a", 0), (b"b", 1)], 9);
1052        assert_ne!(
1053            VocabFingerprint::of(&one).unwrap(),
1054            VocabFingerprint::of(&two).unwrap()
1055        );
1056    }
1057
1058    #[test]
1059    fn a_zero_budget_builds_but_retains_nothing() {
1060        let cache = TrieCache::with_budget(0);
1061        let v = vocab(&[(b"a", 0)], 9);
1062        let built = cache.get_or_build_byte(&v).expect("still built");
1063        assert_eq!(built.node_count(), 2);
1064        assert!(cache.is_empty(), "a zero budget retains no trie");
1065    }
1066
1067    #[test]
1068    fn a_budget_at_one_trie_retains_one_not_two() {
1069        let a = vocab(&[(b"a", 0)], 9);
1070        let budget = VocabTrie::build_byte(&a).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1071        let cache = TrieCache::with_budget(budget);
1072        cache.get_or_build_byte(&a).unwrap(); // fits exactly
1073        cache.get_or_build_byte(&vocab(&[(b"bb", 1)], 9)).unwrap(); // does not fit
1074        assert_eq!(cache.len(), 1, "the budget retains exactly one trie");
1075    }
1076
1077    #[test]
1078    fn entry_overhead_is_charged_so_many_tiny_tries_cannot_exceed_the_budget() {
1079        let payload = VocabTrie::build_byte(&vocab(&[(b"a", 0)], 1000))
1080            .unwrap()
1081            .heap_bytes();
1082        let cache = TrieCache::with_budget(payload * 10);
1083        for i in 0..10u32 {
1084            cache
1085                .get_or_build_byte(&vocab(&[(&[b'a' + i as u8], i)], 1000))
1086                .unwrap();
1087        }
1088        assert!(
1089            cache.len() < 10,
1090            "charging per-entry overhead must retain fewer than a payload-only budget would ({})",
1091            cache.len()
1092        );
1093        assert!(
1094            cache.bytes() <= payload * 10,
1095            "retained bytes stay within budget"
1096        );
1097    }
1098
1099    #[test]
1100    fn single_flight_builds_a_cold_trie_exactly_once_under_concurrency() {
1101        use std::thread;
1102        let cache = Arc::new(TrieCache::new());
1103        let v = Arc::new(vocab(
1104            &[(b"a", 0), (b"bb", 1), (b"ccc", 2), (b"dddd", 3)],
1105            1000,
1106        ));
1107        let ptrs: Vec<usize> = (0..32)
1108            .map(|_| {
1109                let c = cache.clone();
1110                let v = v.clone();
1111                thread::spawn(move || Arc::as_ptr(&c.get_or_build_byte(&v).unwrap()) as usize)
1112            })
1113            .collect::<Vec<_>>()
1114            .into_iter()
1115            .map(|h| h.join().unwrap())
1116            .collect();
1117        assert!(
1118            ptrs.iter().all(|&p| p == ptrs[0]),
1119            "all callers share one trie"
1120        );
1121        let (hits, misses, _, _) = cache.stats();
1122        assert_eq!(misses, 1, "single-flight builds the cold trie exactly once");
1123        assert_eq!(hits, 31, "the other 31 callers hit the one build");
1124        assert_eq!(cache.len(), 1);
1125    }
1126
1127    #[test]
1128    fn single_flight_shares_one_build_even_when_the_budget_retains_nothing() {
1129        use std::sync::Barrier;
1130        use std::thread;
1131        let _serialize = DELAY_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
1132        let cache = Arc::new(TrieCache::with_budget(0));
1133        let v = Arc::new(vocab(
1134            &[(b"a", 0), (b"bb", 1), (b"ccc", 2), (b"dddd", 3)],
1135            1000,
1136        ));
1137        let barrier = Arc::new(Barrier::new(32));
1138        struct ResetDelay;
1139        impl Drop for ResetDelay {
1140            fn drop(&mut self) {
1141                DELAY_BUILDS.store(false, Ordering::Relaxed);
1142            }
1143        }
1144        DELAY_BUILDS.store(true, Ordering::Relaxed);
1145        let _reset = ResetDelay;
1146        let ptrs: Vec<usize> = (0..32)
1147            .map(|_| {
1148                let c = cache.clone();
1149                let v = v.clone();
1150                let b = barrier.clone();
1151                thread::spawn(move || {
1152                    b.wait();
1153                    Arc::as_ptr(&c.get_or_build_byte(&v).unwrap()) as usize
1154                })
1155            })
1156            .collect::<Vec<_>>()
1157            .into_iter()
1158            .map(|h| h.join().unwrap())
1159            .collect();
1160        assert!(
1161            ptrs.iter().all(|&p| p == ptrs[0]),
1162            "all callers share the one build"
1163        );
1164        let (_h, misses, _e, _o) = cache.stats();
1165        assert_eq!(misses, 1, "exactly one build even with nothing retained");
1166        assert!(cache.is_empty(), "a zero budget retains no trie");
1167    }
1168
1169    #[test]
1170    fn concurrent_distinct_builds_are_bounded_by_the_gate() {
1171        use std::thread;
1172        let cache = Arc::new(TrieCache::with_build_limit(1 << 28, 3));
1173        let handles: Vec<_> = (0..24u32)
1174            .map(|i| {
1175                let c = cache.clone();
1176                thread::spawn(move || {
1177                    c.get_or_build_byte(&vocab(
1178                        &[(&[b'a', i as u8], 0), (&[b'b', i as u8, 0], 1)],
1179                        1000,
1180                    ))
1181                    .unwrap();
1182                })
1183            })
1184            .collect();
1185        for h in handles {
1186            h.join().unwrap();
1187        }
1188        assert!(
1189            cache.peak_concurrent_builds() <= 3,
1190            "simultaneous distinct builds must stay within the gate ({})",
1191            cache.peak_concurrent_builds()
1192        );
1193        assert_eq!(cache.len(), 24, "all 24 distinct tries built and retained");
1194    }
1195
1196    #[test]
1197    fn concurrent_builds_are_bounded_by_the_transient_byte_budget() {
1198        use std::sync::Barrier;
1199        use std::thread;
1200        let _serialize = DELAY_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
1201        let sample = vocab(&[(b"xa", 0), (b"xb", 1), (b"xc", 2)], 1000);
1202        let est = estimate_transient_build_bytes(&sample);
1203        let byte_budget = est * 2;
1204        let cache = Arc::new(TrieCache::with_build_gate(1 << 28, 8, byte_budget));
1205        let barrier = Arc::new(Barrier::new(8));
1206        DELAY_BUILDS.store(true, Ordering::Relaxed);
1207        struct ResetDelay;
1208        impl Drop for ResetDelay {
1209            fn drop(&mut self) {
1210                DELAY_BUILDS.store(false, Ordering::Relaxed);
1211            }
1212        }
1213        let _reset = ResetDelay;
1214        let handles: Vec<_> = (0..8u32)
1215            .map(|i| {
1216                let c = cache.clone();
1217                let b = barrier.clone();
1218                thread::spawn(move || {
1219                    b.wait();
1220                    c.get_or_build_byte(&vocab(
1221                        &[
1222                            (&[b'x', i as u8], 0),
1223                            (&[b'y', i as u8], 1),
1224                            (&[b'z', i as u8], 2),
1225                        ],
1226                        1000,
1227                    ))
1228                    .unwrap();
1229                })
1230            })
1231            .collect();
1232        for h in handles {
1233            h.join().unwrap();
1234        }
1235        assert_eq!(
1236            cache.peak_concurrent_builds(),
1237            2,
1238            "the byte budget, not the count cap of 8, must be the active constraint"
1239        );
1240        assert!(cache.peak_concurrent_build_bytes() <= byte_budget);
1241        assert_eq!(
1242            cache.len(),
1243            8,
1244            "all 8 distinct tries eventually built and retained"
1245        );
1246    }
1247
1248    #[test]
1249    fn an_oversized_build_estimate_is_rejected_and_shared_with_every_waiter() {
1250        use std::thread;
1251        let big = vocab(
1252            &[
1253                (b"big-token-one", 0),
1254                (b"big-token-two", 1),
1255                (b"big-token-three", 2),
1256            ],
1257            1000,
1258        );
1259        let est = estimate_transient_build_bytes(&big);
1260        let cache = Arc::new(TrieCache::with_build_gate(1 << 28, 8, est - 1));
1261        let handles: Vec<_> = (0..8)
1262            .map(|_| {
1263                let c = cache.clone();
1264                let v = big.clone();
1265                thread::spawn(move || c.get_or_build_byte(&v).map_err(|e| e.code))
1266            })
1267            .collect();
1268        for h in handles {
1269            let result = h.join().unwrap();
1270            assert_eq!(
1271                result.unwrap_err(),
1272                ErrorCode::InternalLimitExceeded,
1273                "an over-budget build must be rejected, not silently clamped and run"
1274            );
1275        }
1276        assert!(cache.is_empty(), "a rejected build is never cached");
1277    }
1278
1279    #[test]
1280    fn a_hot_vocabulary_becomes_and_stays_cached_after_a_junk_burst_fills_the_cache() {
1281        let sample = vocab(&[(&[b'j', 0, 0], 0)], 1000);
1282        let entry_bytes =
1283            VocabTrie::build_byte(&sample).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1284        let cache = TrieCache::with_budget(entry_bytes * 3);
1285        for i in 0..50u32 {
1286            let junk = vocab(&[(&[b'j', (i % 251) as u8, (i / 251) as u8], i)], 1000);
1287            cache.get_or_build_byte(&junk).unwrap();
1288        }
1289        let (_, _, evictions_from_junk, _) = cache.stats();
1290        assert!(
1291            evictions_from_junk > 0,
1292            "the junk burst overflowed the budget"
1293        );
1294
1295        let hot = vocab(&[(b"hot-token-aaaa", 0), (b"hot-token-bbbb", 1)], 1000);
1296        let (_, misses0, _, _) = cache.stats();
1297        cache.get_or_build_byte(&hot).unwrap();
1298        let (_, misses1, _, _) = cache.stats();
1299        assert_eq!(
1300            misses1,
1301            misses0 + 1,
1302            "the first access to a new hot vocab is a miss"
1303        );
1304
1305        cache.get_or_build_byte(&hot).unwrap();
1306        let (_, misses2, _, _) = cache.stats();
1307        assert_eq!(misses2, misses1, "the second access hits the cached entry");
1308    }
1309
1310    #[test]
1311    fn old_bypass_only_policy_would_fail_the_hot_vocabulary_regression() {
1312        let cache = TrieCache::with_budget(0);
1313        let hot = vocab(&[(b"hot-token-aaaa", 0), (b"hot-token-bbbb", 1)], 1000);
1314        cache.get_or_build_byte(&hot).unwrap();
1315        let (_, misses1, _, _) = cache.stats();
1316        cache.get_or_build_byte(&hot).unwrap();
1317        let (_, misses2, _, _) = cache.stats();
1318        assert_eq!(
1319            misses2,
1320            misses1 + 1,
1321            "a bypass-only cache never turns a repeat into a hit"
1322        );
1323    }
1324
1325    #[test]
1326    fn concurrent_native_threads_share_one_trie_and_never_corrupt_the_cache() {
1327        use std::thread;
1328        let cache = Arc::new(TrieCache::new());
1329        let same = Arc::new(vocab(&[(b"a", 0), (b"bb", 1), (b"ccc", 2)], 1000));
1330        let mut handles = Vec::new();
1331        for _ in 0..16 {
1332            let c = cache.clone();
1333            let v = same.clone();
1334            handles.push(thread::spawn(move || {
1335                c.get_or_build_byte(&v).map(|t| Arc::as_ptr(&t) as usize)
1336            }));
1337        }
1338        for i in 0..16u32 {
1339            let c = cache.clone();
1340            handles.push(thread::spawn(move || {
1341                c.get_or_build_byte(&vocab(&[(&[b'z', i as u8], i)], 1000))
1342                    .map(|t| Arc::as_ptr(&t) as usize)
1343            }));
1344        }
1345        let ptrs: Vec<_> = handles
1346            .into_iter()
1347            .map(|h| h.join().unwrap().unwrap())
1348            .collect();
1349        let shared = Arc::as_ptr(&cache.get_or_build_byte(&same).unwrap()) as usize;
1350        for p in &ptrs[..16] {
1351            assert_eq!(*p, shared, "same vocab must resolve to one cached trie");
1352        }
1353        assert_eq!(
1354            cache.len(),
1355            17,
1356            "one shared trie plus 16 distinct tries retained"
1357        );
1358    }
1359
1360    #[test]
1361    fn clear_drops_retained_tries() {
1362        let cache = TrieCache::new();
1363        cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap();
1364        cache.get_or_build_byte(&vocab(&[(b"b", 0)], 9)).unwrap();
1365        assert_eq!(cache.len(), 2);
1366        cache.clear();
1367        assert!(cache.is_empty());
1368    }
1369
1370    #[test]
1371    fn clear_also_drops_the_lru_log_so_repeated_fill_clear_does_not_leak() {
1372        let cache = TrieCache::new();
1373        for cycle in 0..50u32 {
1374            cache
1375                .get_or_build_byte(&vocab(&[(&[cycle as u8], 0)], 9))
1376                .unwrap();
1377            cache.clear();
1378        }
1379        let lru_len = cache
1380            .state
1381            .lock()
1382            .unwrap_or_else(|p| p.into_inner())
1383            .lru
1384            .len();
1385        assert_eq!(
1386            lru_len, 0,
1387            "clear() must drop the LRU log, not just the map"
1388        );
1389    }
1390
1391    #[test]
1392    fn stats_are_a_lifetime_total_not_reset_by_clear() {
1393        let cache = TrieCache::new();
1394        cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap();
1395        cache.get_or_build_byte(&vocab(&[(b"a", 0)], 9)).unwrap(); // hit
1396        let (hits, misses, ..) = cache.stats();
1397        assert_eq!((hits, misses), (1, 1));
1398        cache.clear();
1399        let (hits_after, misses_after, ..) = cache.stats();
1400        assert_eq!(
1401            (hits_after, misses_after),
1402            (1, 1),
1403            "clear() must not reset traffic counters"
1404        );
1405    }
1406
1407    #[test]
1408    fn an_evicted_trie_stays_valid_and_correct_while_a_caller_still_holds_its_arc() {
1409        let a = vocab(&[(b"a", 0), (b"ab", 1)], 9);
1410        let budget = VocabTrie::build_byte(&a).unwrap().heap_bytes() + CACHE_ENTRY_OVERHEAD;
1411        let cache = TrieCache::with_budget(budget);
1412
1413        let held = cache.get_or_build_byte(&a).unwrap();
1414        assert_eq!(cache.len(), 1);
1415
1416        cache.get_or_build_byte(&vocab(&[(b"z", 0)], 9)).unwrap();
1417        assert_eq!(cache.len(), 1, "the budget still retains exactly one entry");
1418
1419        assert_eq!(
1420            held.node_count(),
1421            3,
1422            "evicted trie's structure is untouched"
1423        );
1424
1425        let rebuilt = cache.get_or_build_byte(&a).unwrap();
1426        assert_eq!(
1427            rebuilt.node_count(),
1428            held.node_count(),
1429            "a rebuild after eviction reproduces the identical structure"
1430        );
1431    }
1432
1433    #[test]
1434    fn a_panicking_build_releases_its_permit_and_slot_so_the_next_build_is_not_stuck() {
1435        let cache = TrieCache::with_build_limit(usize::MAX, 1); // only one concurrent build allowed
1436        let v = vocab(&[(b"a", 0)], 9);
1437
1438        PANIC_ON_BUILD.with(|p| p.set(true));
1439        let result =
1440            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cache.get_or_build_byte(&v)));
1441        PANIC_ON_BUILD.with(|p| p.set(false));
1442        assert!(result.is_err(), "the injected panic must actually unwind");
1443
1444        let recovered = cache
1445            .get_or_build_byte(&v)
1446            .expect("a fresh build must succeed");
1447        assert_eq!(recovered.node_count(), 2);
1448    }
1449
1450    #[test]
1451    fn prepared_transient_estimate_charges_many_ids_under_one_record_like_the_vocab_estimate() {
1452        let mut map: Map<Vec<u8>, Vec<u32>> = Map::default();
1453        map.insert(b"a".to_vec(), (0..5000).collect());
1454        let v = build_vocabulary(6000, map).expect("vocab");
1455        let prepared = PreparedVocabulary::build(&v).expect("prepared");
1456        assert_eq!(
1457            estimate_transient_build_bytes_prepared(&prepared),
1458            estimate_transient_build_bytes(&v)
1459        );
1460    }
1461}