1use 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
14pub const COMPILER_SEMANTICS_VERSION: u32 = 2;
17
18#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
21enum InputKind {
22 JsonSchema,
23 MfirWire,
24 SemanticExecutable,
25}
26
27#[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 #[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 #[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 #[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
166const DEFAULT_ARTIFACT_CACHE_BYTES: usize = 1 << 28;
169
170const 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
177struct BuildSlot {
180 result: Mutex<Option<Result<Arc<CompiledArtifact>, CompileError>>>,
181 ready: Condvar,
182}
183
184struct 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 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; };
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#[derive(Default)]
243struct CacheStats {
244 hits: AtomicU64,
245 misses: AtomicU64,
246 evictions: AtomicU64,
247 oversized_bypasses: AtomicU64,
248}
249
250struct 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
282pub 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 #[must_use]
299 pub fn new() -> Self {
300 Self::default()
301 }
302
303 #[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 #[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 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 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 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; built
453 }
454
455 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 #[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 #[must_use]
477 pub fn is_empty(&self) -> bool {
478 self.len() == 0
479 }
480
481 #[must_use]
484 pub fn bytes(&self) -> usize {
485 self.state.lock().unwrap_or_else(|p| p.into_inner()).bytes
486 }
487
488 #[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 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); 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}