Skip to main content

maskforge_core/runtime/
artifact_cache.rs

1//! Caches compiled artifacts by input and vocabulary identity.
2
3use std::collections::VecDeque;
4use std::hash::{Hash, Hasher};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Condvar, Mutex};
7
8use rustc_hash::FxHashMap;
9
10use super::CompiledArtifact;
11use crate::error::CompileError;
12use crate::index::{BindMode, VocabFingerprint};
13
14/// Bumped whenever a change to IR-compile or bind semantics could make a previously-cached artifact
15/// for the same `(ir_hash, vocab_fingerprint, mask_vocab_size, bind_mode)` no longer equivalent to
16pub const COMPILER_SEMANTICS_VERSION: u32 = 2;
17
18/// Which raw byte representation an [`ArtifactKey`]'s `input_hash` was computed over. Keeps a
19/// JSON-schema-keyed entry and an MFIR-wire-keyed entry from ever comparing equal even if the two
20#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
21enum InputKind {
22    JsonSchema,
23    MfirWire,
24    SemanticExecutable,
25}
26
27/// Content address of one compiled artifact: two vocabularies/schemas that produce equal keys are
28/// guaranteed (by construction, not caller honesty - every field is read from validated state) to
29#[derive(Clone, PartialEq, Eq, Debug)]
30pub(crate) struct ArtifactKey {
31    input_hash: [u8; 32],
32    exact_input: Arc<[u8]>,
33    input_kind: InputKind,
34    vocab_fingerprint: VocabFingerprint,
35    mask_vocab_size: usize,
36    bind_mode: BindMode,
37    compiler_semantics_version: u32,
38}
39
40#[derive(Copy, Clone, PartialEq, Eq)]
41struct ArtifactCandidate {
42    input_hash: [u8; 32],
43    input_kind: InputKind,
44    vocab_fingerprint: VocabFingerprint,
45    mask_vocab_size: usize,
46    bind_mode: BindMode,
47    compiler_semantics_version: u32,
48}
49
50fn hash_input(domain: &[u8], bytes: &[u8]) -> [u8; 32] {
51    let mut h = blake3::Hasher::new();
52    h.update(domain);
53    h.update(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
54    h.update(bytes);
55    *h.finalize().as_bytes()
56}
57
58impl Hash for ArtifactKey {
59    fn hash<H: Hasher>(&self, state: &mut H) {
60        self.candidate().hash(state);
61    }
62}
63
64impl Hash for ArtifactCandidate {
65    fn hash<H: Hasher>(&self, state: &mut H) {
66        self.input_hash.hash(state);
67        self.input_kind.hash(state);
68        self.vocab_fingerprint.hash(state);
69        self.mask_vocab_size.hash(state);
70        self.bind_mode.hash(state);
71        self.compiler_semantics_version.hash(state);
72    }
73}
74
75impl ArtifactKey {
76    /// A key over the exact JSON Schema text and `assume_closed` flag, hashed before any parsing -
77    /// a repeat call with identical bytes is a cache hit that skips schema-to-IR lowering
78    #[must_use]
79    pub fn for_json_schema(
80        schema: &str,
81        assume_closed: bool,
82        vocab_fingerprint: VocabFingerprint,
83        mask_vocab_size: usize,
84        bind_mode: BindMode,
85    ) -> Self {
86        let mut hasher = blake3::Hasher::new();
87        hasher.update(b"MFIR-artifact-key-json-schema-v2");
88        hasher.update(
89            &u64::try_from(schema.len())
90                .unwrap_or(u64::MAX)
91                .to_le_bytes(),
92        );
93        hasher.update(schema.as_bytes());
94        hasher.update(&[u8::from(assume_closed)]);
95        Self {
96            input_hash: *hasher.finalize().as_bytes(),
97            exact_input: Arc::from(schema.as_bytes()),
98            input_kind: InputKind::JsonSchema,
99            vocab_fingerprint,
100            mask_vocab_size,
101            bind_mode,
102            compiler_semantics_version: COMPILER_SEMANTICS_VERSION,
103        }
104    }
105
106    /// A key over the exact validated MFIR wire bytes, hashed before decoding - a repeat call
107    /// skips `SchemaIR::from_wire`'s decode and revalidation on a hit.
108    #[must_use]
109    pub fn for_mfir_wire(
110        wire: &[u8],
111        vocab_fingerprint: VocabFingerprint,
112        mask_vocab_size: usize,
113        bind_mode: BindMode,
114    ) -> Self {
115        Self {
116            input_hash: hash_input(b"MFIR-artifact-key-mfir-wire-v1", wire),
117            exact_input: Arc::from(wire),
118            input_kind: InputKind::MfirWire,
119            vocab_fingerprint,
120            mask_vocab_size,
121            bind_mode,
122            compiler_semantics_version: COMPILER_SEMANTICS_VERSION,
123        }
124    }
125
126    /// A vocabulary binding key over the executable's exact canonical IR identity.
127    #[must_use]
128    pub fn for_executable(
129        semantic_identity: &[u8],
130        vocab_fingerprint: VocabFingerprint,
131        mask_vocab_size: usize,
132        bind_mode: BindMode,
133    ) -> Self {
134        Self {
135            input_hash: hash_input(
136                b"MFIR-artifact-key-semantic-executable-v1",
137                semantic_identity,
138            ),
139            exact_input: Arc::from(semantic_identity),
140            input_kind: InputKind::SemanticExecutable,
141            vocab_fingerprint,
142            mask_vocab_size,
143            bind_mode,
144            compiler_semantics_version: COMPILER_SEMANTICS_VERSION,
145        }
146    }
147
148    fn candidate(&self) -> ArtifactCandidate {
149        ArtifactCandidate {
150            input_hash: self.input_hash,
151            input_kind: self.input_kind,
152            vocab_fingerprint: self.vocab_fingerprint,
153            mask_vocab_size: self.mask_vocab_size,
154            bind_mode: self.bind_mode,
155            compiler_semantics_version: self.compiler_semantics_version,
156        }
157    }
158
159    fn retained_bytes(&self) -> usize {
160        self.exact_input
161            .len()
162            .saturating_add(std::mem::size_of::<Self>())
163    }
164}
165
166/// Default byte budget for retained artifacts: `1 << 28` = 256 MiB. Above it, an artifact is still
167/// built and returned but not retained, so compiling many distinct large schemas cannot grow the
168const DEFAULT_ARTIFACT_CACHE_BYTES: usize = 1 << 28;
169
170/// Conservative per-entry overhead charged ON TOP of an artifact's own retained bytes (the engine's
171/// heap, the packed table, and the eager delta index): the `Arc` control block, the
172const ARTIFACT_CACHE_ENTRY_OVERHEAD: usize = 2 * std::mem::size_of::<usize>()
173    + std::mem::size_of::<CompiledArtifact>()
174    + std::mem::size_of::<ArtifactKey>()
175    + 64;
176
177/// The one builder's result for a key, read by every waiter, so a build the budget does NOT retain
178/// is still not rebuilt once per waiter. `CompileError` is `Clone`, so the error is shared too.
179struct BuildSlot {
180    result: Mutex<Option<Result<Arc<CompiledArtifact>, CompileError>>>,
181    ready: Condvar,
182}
183
184/// A retained artifact with its charged byte size and an LRU access stamp.
185struct CacheEntry {
186    artifact: Arc<CompiledArtifact>,
187    bytes: usize,
188    last_used: u64,
189}
190
191#[derive(Default)]
192struct CacheState {
193    map: FxHashMap<ArtifactKey, CacheEntry>,
194    bytes: usize,
195    tick: u64,
196    lru: VecDeque<(ArtifactCandidate, u64)>,
197    in_flight: FxHashMap<ArtifactKey, Arc<BuildSlot>>,
198}
199
200impl CacheState {
201    fn touch(&mut self, key: &ArtifactKey) -> u64 {
202        self.tick = self.tick.saturating_add(1);
203        let now = self.tick;
204        self.lru.push_back((key.candidate(), now));
205        let compact_at = self.map.len().max(1).saturating_mul(4).saturating_add(64);
206        if self.lru.len() > compact_at {
207            self.compact_lru();
208        }
209        now
210    }
211
212    fn compact_lru(&mut self) {
213        let map = &self.map;
214        self.lru.retain(|(candidate, tick)| {
215            map.iter()
216                .any(|(key, entry)| key.candidate() == *candidate && entry.last_used == *tick)
217        });
218    }
219
220    /// Evicts least-recently-used retained entries until `need` more bytes fit under `budget`. Only
221    /// the cache's `Arc` is dropped; a `Matcher`/caller still holding its own `Arc` stays valid.
222    fn evict_until_fits(&mut self, need: usize, budget: usize, stats: &CacheStats) {
223        while self.bytes.saturating_add(need) > budget {
224            let Some((candidate, tick)) = self.lru.pop_front() else {
225                break; // log exhausted; map empty (the new entry alone exceeds the budget: oversized)
226            };
227            let key = self.map.iter().find_map(|(key, entry)| {
228                (key.candidate() == candidate && entry.last_used == tick).then(|| key.clone())
229            });
230            if let Some(key) = key {
231                if let Some(entry) = self.map.remove(&key) {
232                    self.bytes = self.bytes.saturating_sub(entry.bytes);
233                    stats.evictions.fetch_add(1, Ordering::Relaxed);
234                }
235            }
236        }
237    }
238}
239
240/// Cumulative cache-traffic counters, since creation (not reset by `clear`). Plain atomics, not a
241/// `Mutex`: measured lock contention (`benches/artifact_cache_contention.rs`) showed a second mutex
242#[derive(Default)]
243struct CacheStats {
244    hits: AtomicU64,
245    misses: AtomicU64,
246    evictions: AtomicU64,
247    oversized_bypasses: AtomicU64,
248}
249
250/// Publishes an error to the build slot and clears the in-flight mark on drop UNLESS disarmed, so a
251/// panicking builder never leaves waiters hung on a slot that will never be filled, nor a permanently
252struct SlotGuard<'a> {
253    cache: &'a ArtifactCache,
254    key: ArtifactKey,
255    slot: Arc<BuildSlot>,
256    armed: bool,
257}
258
259impl Drop for SlotGuard<'_> {
260    fn drop(&mut self) {
261        if self.armed {
262            let mut r = self.slot.result.lock().unwrap_or_else(|p| p.into_inner());
263            if r.is_none() {
264                *r = Some(Err(CompileError::new(
265                    crate::error::ErrorCode::InternalLimitExceeded,
266                    crate::error::Stage::L4Bind,
267                    "artifact build failed to publish a result",
268                )));
269            }
270            drop(r);
271            self.slot.ready.notify_all();
272            self.cache
273                .state
274                .lock()
275                .unwrap_or_else(|p| p.into_inner())
276                .in_flight
277                .remove(&self.key);
278        }
279    }
280}
281
282/// A thread-safe, byte-bounded cache of compiled artifacts keyed by [`ArtifactKey`]. Read-mostly
283/// after warm-up; once the budget is reached, further artifacts are built and returned but not
284pub struct ArtifactCache {
285    state: Mutex<CacheState>,
286    stats: CacheStats,
287    budget: usize,
288}
289
290impl Default for ArtifactCache {
291    fn default() -> Self {
292        Self::with_budget(DEFAULT_ARTIFACT_CACHE_BYTES)
293    }
294}
295
296impl ArtifactCache {
297    /// An empty cache with the default byte budget.
298    #[must_use]
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// An empty cache with an explicit retained-byte budget.
304    #[must_use]
305    pub fn with_budget(budget: usize) -> Self {
306        Self {
307            state: Mutex::new(CacheState::default()),
308            stats: CacheStats::default(),
309            budget,
310        }
311    }
312
313    /// The cached artifact for the exact JSON Schema text and options, calling `build` and
314    /// inserting its result on a miss. The key is derived from `schema`/`assume_closed` INSIDE this
315    #[allow(clippy::too_many_arguments)]
316    pub fn get_or_build_json_schema(
317        &self,
318        schema: &str,
319        assume_closed: bool,
320        vocab_fingerprint: VocabFingerprint,
321        mask_vocab_size: usize,
322        bind_mode: BindMode,
323        build: impl FnOnce() -> Result<CompiledArtifact, CompileError>,
324    ) -> Result<Arc<CompiledArtifact>, CompileError> {
325        let key = ArtifactKey::for_json_schema(
326            schema,
327            assume_closed,
328            vocab_fingerprint,
329            mask_vocab_size,
330            bind_mode,
331        );
332        self.get_or_build(key, build)
333    }
334
335    /// Same contract as [`Self::get_or_build_json_schema`], keyed on the exact MFIR wire bytes
336    /// instead of schema text.
337    pub fn get_or_build_mfir_wire(
338        &self,
339        wire: &[u8],
340        vocab_fingerprint: VocabFingerprint,
341        mask_vocab_size: usize,
342        bind_mode: BindMode,
343        build: impl FnOnce() -> Result<CompiledArtifact, CompileError>,
344    ) -> Result<Arc<CompiledArtifact>, CompileError> {
345        let key = ArtifactKey::for_mfir_wire(wire, vocab_fingerprint, mask_vocab_size, bind_mode);
346        self.get_or_build(key, build)
347    }
348
349    /// Returns or binds one exact vocabulary-neutral executable identity.
350    pub fn get_or_build_executable(
351        &self,
352        semantic_identity: &[u8],
353        vocab_fingerprint: VocabFingerprint,
354        mask_vocab_size: usize,
355        bind_mode: BindMode,
356        build: impl FnOnce() -> Result<CompiledArtifact, CompileError>,
357    ) -> Result<Arc<CompiledArtifact>, CompileError> {
358        let key = ArtifactKey::for_executable(
359            semantic_identity,
360            vocab_fingerprint,
361            mask_vocab_size,
362            bind_mode,
363        );
364        self.get_or_build(key, build)
365    }
366
367    /// The cached artifact for `key`, calling `build` and inserting its result on a miss. Every
368    /// bind mode is retained (charged via `artifact_cache_charge_upper_bound`, valid for any mode
369    fn get_or_build(
370        &self,
371        key: ArtifactKey,
372        build: impl FnOnce() -> Result<CompiledArtifact, CompileError>,
373    ) -> Result<Arc<CompiledArtifact>, CompileError> {
374        let slot = {
375            let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
376            if state.map.contains_key(&key) {
377                let now = state.touch(&key);
378                let entry = state
379                    .map
380                    .get_mut(&key)
381                    .expect("just confirmed present under the same lock hold");
382                entry.last_used = now;
383                let artifact = entry.artifact.clone();
384                drop(state);
385                self.stats.hits.fetch_add(1, Ordering::Relaxed);
386                return Ok(artifact);
387            }
388            if let Some(slot) = state.in_flight.get(&key).cloned() {
389                drop(state);
390                let result = {
391                    let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
392                    while r.is_none() {
393                        r = slot.ready.wait(r).unwrap_or_else(|p| p.into_inner());
394                    }
395                    r.clone().expect("slot is ready")
396                };
397                if result.is_ok() {
398                    self.stats.hits.fetch_add(1, Ordering::Relaxed);
399                }
400                return result;
401            }
402            let slot = Arc::new(BuildSlot {
403                result: Mutex::new(None),
404                ready: Condvar::new(),
405            });
406            state.in_flight.insert(key.clone(), slot.clone());
407            slot
408        };
409
410        let mut guard = SlotGuard {
411            cache: self,
412            key: key.clone(),
413            slot: slot.clone(),
414            armed: true,
415        };
416        let built: Result<Arc<CompiledArtifact>, CompileError> = build().map(Arc::new);
417
418        {
419            let mut r = slot.result.lock().unwrap_or_else(|p| p.into_inner());
420            *r = Some(built.clone());
421            slot.ready.notify_all();
422        }
423        {
424            let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
425            state.in_flight.remove(&key);
426            if let Ok(ref arc) = built {
427                self.stats.misses.fetch_add(1, Ordering::Relaxed);
428                let entry_bytes = arc
429                    .artifact_cache_charge_upper_bound()
430                    .saturating_add(key.retained_bytes())
431                    .saturating_add(ARTIFACT_CACHE_ENTRY_OVERHEAD);
432                if entry_bytes > self.budget {
433                    self.stats
434                        .oversized_bypasses
435                        .fetch_add(1, Ordering::Relaxed);
436                } else {
437                    state.evict_until_fits(entry_bytes, self.budget, &self.stats);
438                    let now = state.touch(&key);
439                    state.bytes = state.bytes.saturating_add(entry_bytes);
440                    state.map.insert(
441                        key,
442                        CacheEntry {
443                            artifact: arc.clone(),
444                            bytes: entry_bytes,
445                            last_used: now,
446                        },
447                    );
448                }
449            }
450        }
451        guard.armed = false; // published + cleared cleanly; the guard has nothing to repair
452        built
453    }
454
455    /// Drops every retained artifact. Also clears the LRU touch-log. Traffic counters (`stats()`)
456    /// are NOT reset - they are a lifetime total, not scoped to content currently retained.
457    pub fn clear(&self) {
458        let mut state = self.state.lock().unwrap_or_else(|p| p.into_inner());
459        state.map.clear();
460        state.bytes = 0;
461        state.lru.clear();
462        state.tick = 0;
463    }
464
465    /// The number of retained artifacts.
466    #[must_use]
467    pub fn len(&self) -> usize {
468        self.state
469            .lock()
470            .unwrap_or_else(|p| p.into_inner())
471            .map
472            .len()
473    }
474
475    /// Whether the cache retains no artifact.
476    #[must_use]
477    pub fn is_empty(&self) -> bool {
478        self.len() == 0
479    }
480
481    /// Retained artifact bytes. Bounds the CACHE only - an artifact a `Matcher` still holds its own
482    /// `Arc` to stays alive via that reference even if evicted or never cached here.
483    #[must_use]
484    pub fn bytes(&self) -> usize {
485        self.state.lock().unwrap_or_else(|p| p.into_inner()).bytes
486    }
487
488    /// `(hits, misses, evictions, oversized_bypasses)` since creation - a lifetime traffic total,
489    /// NOT reset by `clear()`. `oversized_bypasses` is a built artifact whose conservative upper
490    #[must_use]
491    pub fn stats(&self) -> (u64, u64, u64, u64) {
492        (
493            self.stats.hits.load(Ordering::Relaxed),
494            self.stats.misses.load(Ordering::Relaxed),
495            self.stats.evictions.load(Ordering::Relaxed),
496            self.stats.oversized_bypasses.load(Ordering::Relaxed),
497        )
498    }
499}
500
501#[cfg(test)]
502mod key_tests {
503    use super::*;
504
505    fn fp() -> VocabFingerprint {
506        let mut m: rustc_hash::FxHashMap<Vec<u8>, Vec<u32>> = rustc_hash::FxHashMap::default();
507        m.insert(b"a".to_vec(), vec![0]);
508        let v = crate::vocab::build_vocabulary(9, m).expect("vocab");
509        VocabFingerprint::of(&v).expect("fingerprint")
510    }
511
512    #[test]
513    fn json_schema_key_is_deterministic() {
514        let a = ArtifactKey::for_json_schema("true", false, fp(), 10, BindMode::Naive);
515        let b = ArtifactKey::for_json_schema("true", false, fp(), 10, BindMode::Naive);
516        assert_eq!(a, b);
517    }
518
519    #[test]
520    fn json_schema_key_distinguishes_schema_text() {
521        let a = ArtifactKey::for_json_schema("true", false, fp(), 10, BindMode::Naive);
522        let b = ArtifactKey::for_json_schema("false", false, fp(), 10, BindMode::Naive);
523        assert_ne!(a, b);
524    }
525
526    #[test]
527    fn json_schema_key_distinguishes_assume_closed() {
528        let a = ArtifactKey::for_json_schema("true", false, fp(), 10, BindMode::Naive);
529        let b = ArtifactKey::for_json_schema("true", true, fp(), 10, BindMode::Naive);
530        assert_ne!(
531            a, b,
532            "assume_closed changes IR semantics; the key must not collapse it"
533        );
534    }
535
536    #[test]
537    fn mfir_wire_key_is_deterministic_and_distinguishes_bytes() {
538        let a = ArtifactKey::for_mfir_wire(b"one", fp(), 10, BindMode::Naive);
539        let b = ArtifactKey::for_mfir_wire(b"one", fp(), 10, BindMode::Naive);
540        let c = ArtifactKey::for_mfir_wire(b"two", fp(), 10, BindMode::Naive);
541        assert_eq!(a, b);
542        assert_ne!(a, c);
543    }
544
545    #[test]
546    fn json_schema_and_mfir_wire_keys_never_collide_on_the_same_bytes() {
547        let json_key = ArtifactKey::for_json_schema("true", false, fp(), 10, BindMode::Naive);
548        let wire_key = ArtifactKey::for_mfir_wire(b"true\0", fp(), 10, BindMode::Naive);
549        assert_ne!(json_key, wire_key);
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use crate::automaton::RefEngine;
557    use crate::correctness::corpus::corpus_cases;
558    use crate::error::{HashState, Provenance};
559    use crate::index::{VocabTrie, VocabularyHandle};
560    use crate::vocab::{build_vocabulary, Vocabulary};
561    use rustc_hash::FxHashMap as Map;
562
563    fn boolean_engine() -> RefEngine {
564        corpus_cases()
565            .expect("corpus")
566            .into_iter()
567            .find(|c| c.name == "suite-type-boolean")
568            .expect("boolean case")
569            .engine
570    }
571
572    fn vocab(pairs: &[(&[u8], u32)], eos: u32) -> Vocabulary {
573        let mut m: Map<Vec<u8>, Vec<u32>> = Map::default();
574        for &(b, id) in pairs {
575            m.entry(b.to_vec()).or_default().push(id);
576        }
577        build_vocabulary(eos, m).expect("vocab")
578    }
579
580    fn key_for(v: &Vocabulary, ir_hash: [u8; 32]) -> ArtifactKey {
581        let fp = VocabFingerprint::of(v).expect("fingerprint");
582        ArtifactKey::for_mfir_wire(&ir_hash, fp, v.eos_token_id() as usize + 1, BindMode::Naive)
583    }
584
585    /// A key matching what `packed_artifact` below actually produces.
586    fn packed_key_for(v: &Vocabulary, ir_hash: [u8; 32]) -> ArtifactKey {
587        let fp = VocabFingerprint::of(v).expect("fingerprint");
588        ArtifactKey::for_mfir_wire(
589            &ir_hash,
590            fp,
591            v.eos_token_id() as usize + 1,
592            BindMode::TrieJointBytePacked,
593        )
594    }
595
596    fn packed_artifact(v: &Vocabulary, ir_hash: [u8; 32]) -> CompiledArtifact {
597        let handle = VocabularyHandle::new(Arc::new(v.clone())).expect("handle");
598        let trie = VocabTrie::build_byte(v).expect("trie");
599        CompiledArtifact::new_packed_from_trie(
600            boolean_engine(),
601            handle.canonical().clone(),
602            Provenance::reference(HashState::Hash(ir_hash)),
603            &trie,
604            handle.fingerprint(),
605        )
606        .expect("packed artifact")
607    }
608
609    fn lazy_key_for(v: &Vocabulary, ir_hash: [u8; 32]) -> ArtifactKey {
610        let fp = VocabFingerprint::of(v).expect("fingerprint");
611        ArtifactKey::for_mfir_wire(
612            &ir_hash,
613            fp,
614            v.eos_token_id() as usize + 1,
615            BindMode::TrieJointByteLazy,
616        )
617    }
618
619    fn lazy_artifact(v: &Vocabulary, ir_hash: [u8; 32]) -> CompiledArtifact {
620        let handle = VocabularyHandle::new(Arc::new(v.clone())).expect("handle");
621        let trie = VocabTrie::build_byte(v).expect("trie");
622        CompiledArtifact::new_lazy_trie(
623            boolean_engine(),
624            handle.canonical().clone(),
625            Provenance::reference(HashState::Hash(ir_hash)),
626            Arc::new(trie),
627            handle.fingerprint(),
628        )
629        .expect("lazy artifact")
630    }
631
632    #[test]
633    fn a_second_identical_key_is_a_hit_not_a_rebuild() {
634        let cache = ArtifactCache::new();
635        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
636        let key = packed_key_for(&v, [1; 32]);
637        let mut builds = 0;
638        let a = cache
639            .get_or_build(key.clone(), || {
640                builds += 1;
641                Ok(packed_artifact(&v, [1; 32]))
642            })
643            .unwrap();
644        let b = cache
645            .get_or_build(key.clone(), || {
646                builds += 1;
647                Ok(packed_artifact(&v, [1; 32]))
648            })
649            .unwrap();
650        assert!(Arc::ptr_eq(&a, &b), "a cache hit must reuse the Arc");
651        assert_eq!(builds, 1, "the second call must not rebuild");
652        let (hits, misses, _, _) = cache.stats();
653        assert_eq!(hits, 1);
654        assert_eq!(misses, 1);
655    }
656
657    #[test]
658    fn distinct_ir_hashes_are_distinct_entries() {
659        let cache = ArtifactCache::new();
660        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
661        let key_a = packed_key_for(&v, [1; 32]);
662        let key_b = packed_key_for(&v, [2; 32]);
663        let a = cache
664            .get_or_build(key_a, || Ok(packed_artifact(&v, [1; 32])))
665            .unwrap();
666        let b = cache
667            .get_or_build(key_b, || Ok(packed_artifact(&v, [2; 32])))
668            .unwrap();
669        assert!(!Arc::ptr_eq(&a, &b));
670        assert_eq!(cache.len(), 2);
671    }
672
673    #[test]
674    fn a_non_packed_result_is_retained_under_its_conservative_upper_bound() {
675        let cache = ArtifactCache::new();
676        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
677        let key = key_for(&v, [1; 32]);
678        let mut builds = 0;
679        let a = cache
680            .get_or_build(key.clone(), || {
681                builds += 1;
682                CompiledArtifact::new(
683                    boolean_engine(),
684                    Arc::new(v.clone()),
685                    Provenance::reference(HashState::Hash([1; 32])),
686                )
687            })
688            .unwrap();
689        let b = cache
690            .get_or_build(key.clone(), || {
691                builds += 1;
692                CompiledArtifact::new(
693                    boolean_engine(),
694                    Arc::new(v.clone()),
695                    Provenance::reference(HashState::Hash([1; 32])),
696                )
697            })
698            .unwrap();
699        assert!(
700            Arc::ptr_eq(&a, &b),
701            "a repeat naive-mode build must be a real cache hit"
702        );
703        assert_eq!(builds, 1);
704        assert_eq!(cache.len(), 1);
705        let (hits, misses, _, _) = cache.stats();
706        assert_eq!(hits, 1);
707        assert_eq!(misses, 1);
708    }
709
710    #[test]
711    fn an_adaptively_lazy_result_is_retained_and_hit_on_repeat() {
712        let cache = ArtifactCache::new();
713        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
714        let key = lazy_key_for(&v, [6; 32]);
715        let mut builds = 0;
716        let a = cache
717            .get_or_build(key.clone(), || {
718                builds += 1;
719                Ok(lazy_artifact(&v, [6; 32]))
720            })
721            .unwrap();
722        let b = cache
723            .get_or_build(key, || {
724                builds += 1;
725                Ok(lazy_artifact(&v, [6; 32]))
726            })
727            .unwrap();
728        assert!(
729            Arc::ptr_eq(&a, &b),
730            "an adaptive-lazy result must hit on repeat, not rebuild"
731        );
732        assert_eq!(
733            builds, 1,
734            "an identical deep schema must not silently rebuild forever"
735        );
736    }
737
738    #[test]
739    fn concurrent_identical_lazy_misses_build_exactly_once() {
740        let cache = ArtifactCache::new();
741        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
742        let key = lazy_key_for(&v, [7; 32]);
743        let builds = AtomicU64::new(0);
744        let results: Vec<Arc<CompiledArtifact>> = std::thread::scope(|scope| {
745            let handles: Vec<_> = (0..8)
746                .map(|_| {
747                    let cache = &cache;
748                    let v = &v;
749                    let builds = &builds;
750                    let key = key.clone();
751                    scope.spawn(move || {
752                        cache
753                            .get_or_build(key, || {
754                                builds.fetch_add(1, Ordering::SeqCst);
755                                std::thread::sleep(std::time::Duration::from_millis(5));
756                                Ok(lazy_artifact(v, [7; 32]))
757                            })
758                            .unwrap()
759                    })
760                })
761                .collect();
762            handles.into_iter().map(|h| h.join().unwrap()).collect()
763        });
764        assert_eq!(
765            builds.load(Ordering::SeqCst),
766            1,
767            "8 concurrent identical lazy misses must build exactly once"
768        );
769        for r in &results[1..] {
770            assert!(
771                Arc::ptr_eq(&results[0], r),
772                "every waiter must get the same Arc"
773            );
774        }
775    }
776
777    #[test]
778    fn a_build_error_is_not_cached_and_a_later_call_can_still_succeed() {
779        let cache = ArtifactCache::new();
780        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
781        let key = packed_key_for(&v, [3; 32]);
782        let err = cache
783            .get_or_build(key.clone(), || {
784                Err(CompileError::new(
785                    crate::error::ErrorCode::Unsupported,
786                    crate::error::Stage::L4Bind,
787                    "injected",
788                ))
789            })
790            .unwrap_err();
791        assert_eq!(err.code, crate::error::ErrorCode::Unsupported);
792        assert_eq!(cache.len(), 0, "a failed build must not be retained");
793        let ok = cache
794            .get_or_build(key, || Ok(packed_artifact(&v, [3; 32])))
795            .unwrap();
796        assert_eq!(cache.len(), 1);
797        let _ = ok;
798    }
799
800    #[test]
801    fn an_oversized_artifact_is_returned_but_not_retained() {
802        let cache = ArtifactCache::with_budget(1); // impossibly small: every entry is oversized
803        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
804        let key = packed_key_for(&v, [4; 32]);
805        let a = cache
806            .get_or_build(key, || Ok(packed_artifact(&v, [4; 32])))
807            .unwrap();
808        drop(a);
809        assert_eq!(cache.len(), 0);
810        let (_, _, _, oversized) = cache.stats();
811        assert_eq!(oversized, 1);
812    }
813
814    #[test]
815    fn clear_drops_retained_entries_but_not_lifetime_stats() {
816        let cache = ArtifactCache::new();
817        let v = vocab(&[(b"a", 0), (b"b", 1)], 9);
818        let key = packed_key_for(&v, [5; 32]);
819        cache
820            .get_or_build(key, || Ok(packed_artifact(&v, [5; 32])))
821            .unwrap();
822        assert_eq!(cache.len(), 1);
823        cache.clear();
824        assert_eq!(cache.len(), 0);
825        let (_, misses, _, _) = cache.stats();
826        assert_eq!(misses, 1, "clear must not reset lifetime traffic counters");
827    }
828}