1pub mod go_stdlib;
2pub mod prelude;
3pub mod types;
4
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6use std::fs;
7use std::hash::{Hash, Hasher};
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use serde::{Deserialize, Serialize};
13use syntax::program::File;
14
15use crate::store::{ENTRY_MODULE_ID, Store};
16use types::CachedDefinition;
17
18pub const CACHE_FORMAT_VERSION: u32 = 1;
20
21pub const COMPILER_VERSION_HASH: u64 = const_fnv1a_hash(env!("CARGO_PKG_VERSION").as_bytes());
23
24pub const STDLIB_HASH: u64 = stdlib::STDLIB_CONTENT_HASH;
27
28pub const PRELUDE_HASH: u64 = stdlib::PRELUDE_CONTENT_HASH;
30
31pub const GO_STDLIB_HASH: u64 = stdlib::GO_STD_CONTENT_HASH;
33
34const FNV_OFFSET: u64 = 0xcbf29ce484222325;
35const FNV_PRIME: u64 = 0x100000001b3;
36
37const fn const_fnv1a_hash(bytes: &[u8]) -> u64 {
39 let mut hash = FNV_OFFSET;
40 let mut i = 0;
41 while i < bytes.len() {
42 hash ^= bytes[i] as u64;
43 hash = hash.wrapping_mul(FNV_PRIME);
44 i += 1;
45 }
46 hash
47}
48
49struct FnvHasher(u64);
52
53impl FnvHasher {
54 fn new() -> Self {
55 Self(FNV_OFFSET)
56 }
57}
58
59impl Hasher for FnvHasher {
60 fn write(&mut self, bytes: &[u8]) {
61 for &byte in bytes {
62 self.0 ^= byte as u64;
63 self.0 = self.0.wrapping_mul(FNV_PRIME);
64 }
65 }
66
67 fn finish(&self) -> u64 {
68 self.0
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ModuleInterface {
74 pub version: u32,
75
76 pub compiler_version: u64,
77
78 pub stdlib_hash: u64,
79
80 pub module_hash: u64,
83
84 pub source_hash: u64,
85
86 pub dependency_hashes: HashMap<String, u64>,
88
89 pub files: Vec<CachedFile>,
90
91 pub definitions: HashMap<String, CachedDefinition>,
92
93 pub ufcs_methods: Vec<(String, String)>,
95
96 pub emit_stamp: Option<u64>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CachedFile {
105 pub name: String,
106 pub source: String,
107}
108
109#[derive(Debug, Clone)]
110pub struct CompiledModule {
111 pub module_id: String,
112 pub source_hash: u64,
113 pub dep_hashes: HashMap<String, u64>,
114}
115
116#[derive(Debug, Clone)]
117pub struct EmitStamp {
118 pub module_id: String,
119 pub artifact_hash: u64,
120}
121
122pub fn compute_emit_artifact_hash(source_hash: u64, go_module: &str) -> u64 {
124 let mut hasher = FnvHasher::new();
125 source_hash.hash(&mut hasher);
126 go_module.hash(&mut hasher);
127 hasher.finish()
128}
129
130pub fn hash_module_sources(files: &[File]) -> u64 {
131 let mut hasher = FnvHasher::new();
132
133 let mut sorted: Vec<_> = files.iter().collect();
134 sorted.sort_by_key(|f| &f.name);
135
136 for file in sorted {
137 file.name.hash(&mut hasher);
138 file.source.hash(&mut hasher);
139 }
140
141 hasher.finish()
142}
143
144pub fn compute_module_hash(source_hash: u64, dep_hashes: &HashMap<String, u64>) -> u64 {
148 let mut hasher = FnvHasher::new();
149 source_hash.hash(&mut hasher);
150
151 let mut deps: Vec<_> = dep_hashes.iter().collect();
152 deps.sort_by_key(|(k, _)| *k);
153 for (name, hash) in deps {
154 name.hash(&mut hasher);
155 hash.hash(&mut hasher);
156 }
157
158 hasher.finish()
159}
160
161pub fn get_dependency_module_hashes(
162 module_id: &str,
163 edges: &HashMap<String, HashSet<String>>,
164 module_hashes: &HashMap<String, u64>,
165) -> HashMap<String, u64> {
166 let Some(deps) = edges.get(module_id) else {
167 return HashMap::default();
168 };
169
170 deps.iter()
171 .map(|dep_id| {
172 let hash = if dep_id.starts_with("go:") || dep_id == "prelude" {
173 STDLIB_HASH
174 } else {
175 *module_hashes.get(dep_id).unwrap_or(&0)
176 };
177 (dep_id.clone(), hash)
178 })
179 .collect()
180}
181
182pub fn is_cache_valid(
183 cache: &ModuleInterface,
184 current_source_hash: u64,
185 current_dep_hashes: &HashMap<String, u64>,
186) -> bool {
187 cache.version == CACHE_FORMAT_VERSION
188 && cache.compiler_version == COMPILER_VERSION_HASH
189 && cache.stdlib_hash == STDLIB_HASH
190 && cache.source_hash == current_source_hash
191 && cache.dependency_hashes == *current_dep_hashes
192}
193
194pub fn cache_path(project_root: &Path, module_id: &str) -> PathBuf {
195 project_root
196 .join("target")
197 .join("cache")
198 .join(cache_file_name(module_id))
199}
200
201pub fn cache_file_name(module_id: &str) -> String {
202 format!("{}.cache", module_id.replace('/', "_"))
203}
204
205pub fn try_load_cache(
206 module_id: &str,
207 expected_source_hash: u64,
208 expected_dep_hashes: &HashMap<String, u64>,
209 expected_artifact_hash: Option<u64>,
210 project_root: &Path,
211 check_go_files: bool,
212) -> Option<ModuleInterface> {
213 let path = cache_path(project_root, module_id);
214 let bytes = fs::read(&path).ok()?;
215 let interface: ModuleInterface = match bincode::deserialize(&bytes) {
216 Ok(i) => i,
217 Err(_) => {
218 let _ = fs::remove_file(&path);
219 return None;
220 }
221 };
222
223 if !is_cache_valid(&interface, expected_source_hash, expected_dep_hashes) {
224 let _ = fs::remove_file(&path);
225 return None;
226 }
227
228 if check_go_files {
229 if interface.emit_stamp != expected_artifact_hash {
230 return None;
231 }
232 if !all_go_outputs_exist(module_id, &interface.files, project_root) {
233 return None;
234 }
235 }
236
237 Some(interface)
238}
239
240fn all_go_outputs_exist(module_id: &str, cached_files: &[CachedFile], project_root: &Path) -> bool {
241 let target_dir = if module_id == ENTRY_MODULE_ID {
242 project_root.join("target")
243 } else {
244 project_root.join("target").join(module_id)
245 };
246
247 for cached_file in cached_files {
248 if cached_file.name.ends_with(".lis") && !cached_file.name.ends_with(".d.lis") {
249 let go_name = cached_file.name.replace(".lis", ".go");
250 if !target_dir.join(&go_name).exists() {
251 return false;
252 }
253 }
254 }
255
256 true
257}
258
259pub fn save_module_cache(
260 compiled: &CompiledModule,
261 store: &Store,
262 project_root: &Path,
263 ufcs_methods: &HashSet<(String, String)>,
264) -> io::Result<()> {
265 let module_hash = compute_module_hash(compiled.source_hash, &compiled.dep_hashes);
266
267 let Some(module) = store.get_module(&compiled.module_id) else {
268 return Err(io::Error::other("module not found in store"));
269 };
270
271 let mut all_files: Vec<_> = module
272 .files
273 .values()
274 .chain(module.typedefs.values())
275 .collect();
276 all_files.sort_by_key(|f| &f.name);
277
278 let file_id_to_index: HashMap<u32, u32> = all_files
279 .iter()
280 .enumerate()
281 .map(|(idx, f)| (f.id, idx as u32))
282 .collect();
283
284 let interface = ModuleInterface {
285 version: CACHE_FORMAT_VERSION,
286 compiler_version: COMPILER_VERSION_HASH,
287 stdlib_hash: STDLIB_HASH,
288 module_hash,
289 source_hash: compiled.source_hash,
290 dependency_hashes: compiled.dep_hashes.clone(),
291 files: all_files
292 .iter()
293 .map(|f| CachedFile {
294 name: f.name.clone(),
295 source: f.source.clone(),
296 })
297 .collect(),
298 definitions: extract_public_definitions(store, &compiled.module_id, &file_id_to_index),
299 ufcs_methods: {
300 let prefix = format!("{}.", compiled.module_id);
301 ufcs_methods
302 .iter()
303 .filter(|(type_id, _)| type_id.starts_with(&prefix))
304 .cloned()
305 .collect()
306 },
307 emit_stamp: None,
308 };
309
310 let path = cache_path(project_root, &compiled.module_id);
311 if let Some(parent) = path.parent() {
312 fs::create_dir_all(parent)?;
313 }
314
315 let temp_path = path.with_extension("cache.tmp");
317 let bytes = bincode::serialize(&interface).map_err(io::Error::other)?;
318 fs::write(&temp_path, bytes)?;
319 fs::rename(&temp_path, &path)?;
320
321 Ok(())
322}
323
324fn extract_public_definitions(
325 store: &Store,
326 module_id: &str,
327 file_id_to_index: &HashMap<u32, u32>,
328) -> HashMap<String, CachedDefinition> {
329 let Some(module) = store.get_module(module_id) else {
330 return HashMap::default();
331 };
332
333 module
334 .definitions
335 .iter()
336 .filter(|(_, definition)| definition.visibility().is_public())
337 .map(|(name, definition)| {
338 (
339 name.to_string(),
340 CachedDefinition::from_definition(definition, file_id_to_index),
341 )
342 })
343 .collect()
344}
345
346pub fn register_cached_module(
351 store: &mut Store,
352 module_id: &str,
353 cached: ModuleInterface,
354 project_root: &Path,
355) {
356 store.add_module(module_id);
357
358 let mut file_ids: Vec<u32> = vec![];
359 for cached_file in &cached.files {
360 let file_id = store.new_file_id();
361 file_ids.push(file_id);
362
363 let display_path = cached_file_display_path(project_root, module_id, &cached_file.name);
364 let file = File::new_cached(
365 module_id,
366 &cached_file.name,
367 &display_path,
368 &cached_file.source,
369 file_id,
370 );
371
372 store.store_file(module_id, file);
373 }
374
375 let module = store.get_module_mut(module_id).unwrap();
376 for (qualified_name, cached_definition) in cached.definitions {
377 let definition = cached_definition.to_definition(&file_ids);
378 module.definitions.insert(qualified_name.into(), definition);
379 }
380
381 store.mark_visited(module_id);
382}
383
384fn cached_file_display_path(project_root: &Path, module_id: &str, bare_name: &str) -> String {
385 let on_disk = if module_id == ENTRY_MODULE_ID {
386 project_root.join("src").join(bare_name)
387 } else {
388 project_root.join("src").join(module_id).join(bare_name)
389 };
390 crate::path::relative_to_cwd(&on_disk).unwrap_or_else(|| bare_name.to_string())
391}
392
393pub fn apply_emit_stamps(
398 project_root: &Path,
399 updates: &[(EmitStamp, Option<u64>)],
400) -> io::Result<()> {
401 for (stamp, value) in updates {
402 let path = cache_path(project_root, &stamp.module_id);
403 let bytes = match fs::read(&path) {
404 Ok(b) => b,
405 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
406 Err(e) => return Err(e),
407 };
408 let mut interface: ModuleInterface = match bincode::deserialize(&bytes) {
409 Ok(i) => i,
410 Err(_) => {
411 let _ = fs::remove_file(&path);
412 continue;
413 }
414 };
415 interface.emit_stamp = *value;
416
417 let temp_path = path.with_extension("cache.tmp");
418 let new_bytes = bincode::serialize(&interface).map_err(io::Error::other)?;
419 fs::write(&temp_path, new_bytes)?;
420 fs::rename(&temp_path, &path)?;
421 }
422 Ok(())
423}
424
425pub fn is_cache_disabled() -> bool {
426 std::env::var("LISETTE_NO_CACHE")
427 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
428 .unwrap_or(false)
429}
430
431static GLOBAL_CACHE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
432
433pub(crate) fn global_cache_temp_path(final_path: &Path) -> PathBuf {
434 let counter = GLOBAL_CACHE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
435 final_path.with_extension(format!("tmp.{}.{}", std::process::id(), counter))
436}
437
438pub(crate) fn prune_legacy_global_caches(dir: &Path, prefix: &str) {
439 let Ok(entries) = fs::read_dir(dir) else {
440 return;
441 };
442 for entry in entries.flatten() {
443 let name = entry.file_name();
444 let name = name.to_string_lossy();
445 if name.starts_with(prefix) && name.contains("_compiler_") {
446 let _ = fs::remove_file(entry.path());
447 }
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use syntax::types::{Symbol, Type};
455
456 #[test]
457 fn test_hash_module_sources_deterministic() {
458 let file1 = File::new_cached("mod", "a.lis", "a.lis", "fn foo() {}", 1);
459 let file2 = File::new_cached("mod", "b.lis", "b.lis", "fn bar() {}", 2);
460
461 let hash1 = hash_module_sources(&[file1.clone(), file2.clone()]);
462 let hash2 = hash_module_sources(&[file2.clone(), file1.clone()]);
463
464 assert_eq!(hash1, hash2);
465 }
466
467 #[test]
468 fn test_hash_module_sources_content_sensitive() {
469 let file1 = File::new_cached("mod", "a.lis", "a.lis", "fn foo() {}", 1);
470 let file2 = File::new_cached("mod", "a.lis", "a.lis", "fn bar() {}", 1);
471
472 let hash1 = hash_module_sources(&[file1]);
473 let hash2 = hash_module_sources(&[file2]);
474
475 assert_ne!(hash1, hash2);
476 }
477
478 #[test]
479 fn test_compute_module_hash_includes_deps() {
480 let source_hash = 12345u64;
481 let mut deps1 = HashMap::default();
482 deps1.insert("dep_a".to_string(), 111u64);
483
484 let mut deps2 = HashMap::default();
485 deps2.insert("dep_a".to_string(), 222u64);
486
487 let hash1 = compute_module_hash(source_hash, &deps1);
488 let hash2 = compute_module_hash(source_hash, &deps2);
489
490 assert_ne!(hash1, hash2);
491 }
492
493 #[test]
494 fn test_compute_module_hash_deterministic() {
495 let source_hash = 12345u64;
496 let mut deps = HashMap::default();
497 deps.insert("dep_b".to_string(), 222u64);
498 deps.insert("dep_a".to_string(), 111u64);
499
500 let hash1 = compute_module_hash(source_hash, &deps);
501 let hash2 = compute_module_hash(source_hash, &deps);
502
503 assert_eq!(hash1, hash2);
504 }
505
506 #[test]
507 fn test_cache_validity_checks_version() {
508 let cache = ModuleInterface {
509 version: CACHE_FORMAT_VERSION + 1, compiler_version: COMPILER_VERSION_HASH,
511 stdlib_hash: STDLIB_HASH,
512 module_hash: 0,
513 source_hash: 100,
514 dependency_hashes: HashMap::default(),
515 files: vec![],
516 definitions: HashMap::default(),
517 ufcs_methods: vec![],
518 emit_stamp: None,
519 };
520
521 assert!(!is_cache_valid(&cache, 100, &HashMap::default()));
522 }
523
524 #[test]
525 fn test_cache_validity_checks_compiler_version() {
526 let cache = ModuleInterface {
527 version: CACHE_FORMAT_VERSION,
528 compiler_version: COMPILER_VERSION_HASH + 1, stdlib_hash: STDLIB_HASH,
530 module_hash: 0,
531 source_hash: 100,
532 dependency_hashes: HashMap::default(),
533 files: vec![],
534 definitions: HashMap::default(),
535 ufcs_methods: vec![],
536 emit_stamp: None,
537 };
538
539 assert!(!is_cache_valid(&cache, 100, &HashMap::default()));
540 }
541
542 #[test]
543 fn test_cache_validity_checks_source_hash() {
544 let cache = ModuleInterface {
545 version: CACHE_FORMAT_VERSION,
546 compiler_version: COMPILER_VERSION_HASH,
547 stdlib_hash: STDLIB_HASH,
548 module_hash: 0,
549 source_hash: 100,
550 dependency_hashes: HashMap::default(),
551 files: vec![],
552 definitions: HashMap::default(),
553 ufcs_methods: vec![],
554 emit_stamp: None,
555 };
556
557 assert!(!is_cache_valid(&cache, 200, &HashMap::default()));
558 assert!(is_cache_valid(&cache, 100, &HashMap::default()));
559 }
560
561 #[test]
562 fn test_cache_validity_checks_dep_hashes() {
563 let mut cached_deps = HashMap::default();
564 cached_deps.insert("dep".to_string(), 111u64);
565
566 let cache = ModuleInterface {
567 version: CACHE_FORMAT_VERSION,
568 compiler_version: COMPILER_VERSION_HASH,
569 stdlib_hash: STDLIB_HASH,
570 module_hash: 0,
571 source_hash: 100,
572 dependency_hashes: cached_deps.clone(),
573 files: vec![],
574 definitions: HashMap::default(),
575 ufcs_methods: vec![],
576 emit_stamp: None,
577 };
578
579 let mut different_deps = HashMap::default();
580 different_deps.insert("dep".to_string(), 222u64);
581
582 assert!(!is_cache_valid(&cache, 100, &different_deps));
583 assert!(is_cache_valid(&cache, 100, &cached_deps));
584 }
585
586 #[test]
587 fn test_type_roundtrip_bincode() {
588 let ty = Type::Function {
589 params: vec![Type::Nominal {
590 id: Symbol::from_raw("int"),
591 params: vec![],
592 underlying_ty: None,
593 }],
594 param_mutability: vec![false],
595 bounds: vec![],
596 return_type: Box::new(Type::Nominal {
597 id: Symbol::from_raw("main.MyType"),
598 params: vec![Type::Tuple(vec![Type::Never])],
599 underlying_ty: None,
600 }),
601 };
602
603 let bytes = bincode::serialize(&ty).unwrap();
604 let restored: Type = bincode::deserialize(&bytes).unwrap();
605 assert_eq!(ty, restored);
606 }
607
608 #[test]
609 fn test_cache_path_format() {
610 let path = cache_path(Path::new("/project"), "utils");
611 assert_eq!(path, PathBuf::from("/project/target/cache/utils.cache"));
612
613 let path = cache_path(Path::new("/project"), "deep/nested/mod");
614 assert_eq!(
615 path,
616 PathBuf::from("/project/target/cache/deep_nested_mod.cache")
617 );
618 }
619
620 #[test]
621 fn test_get_dependency_module_hashes_uses_stdlib_hash() {
622 let mut edges = HashMap::default();
623 let mut deps = HashSet::default();
624 deps.insert("go:fmt".to_string());
625 deps.insert("prelude".to_string());
626 deps.insert("user_mod".to_string());
627 edges.insert("my_mod".to_string(), deps);
628
629 let mut module_hashes = HashMap::default();
630 module_hashes.insert("user_mod".to_string(), 12345u64);
631
632 let result = get_dependency_module_hashes("my_mod", &edges, &module_hashes);
633
634 assert_eq!(result.get("go:fmt"), Some(&STDLIB_HASH));
635 assert_eq!(result.get("prelude"), Some(&STDLIB_HASH));
636 assert_eq!(result.get("user_mod"), Some(&12345u64));
637 }
638
639 #[test]
640 fn hash_module_sources_independent_of_display_path() {
641 let cli_file = File::new(
642 "greet",
643 "greet.lis",
644 "src/greet/greet.lis",
645 "pub fn x() -> int { 1 }",
646 vec![],
647 1,
648 );
649 let lsp_file = File::new(
650 "greet",
651 "greet.lis",
652 "greet.lis",
653 "pub fn x() -> int { 1 }",
654 vec![],
655 1,
656 );
657
658 assert_eq!(
659 hash_module_sources(&[cli_file]),
660 hash_module_sources(&[lsp_file]),
661 );
662 }
663
664 #[test]
665 fn cache_file_purity_no_src_prefix() {
666 let cached = CachedFile {
667 name: "greet.lis".to_string(),
668 source: "pub fn x() -> int { 1 }".to_string(),
669 };
670 let bytes = bincode::serialize(&cached).unwrap();
671 let serialized = String::from_utf8_lossy(&bytes);
672 assert!(
673 !serialized.contains("src/"),
674 "CachedFile must not contain `src/` prefix; got: {serialized:?}"
675 );
676 }
677
678 #[test]
679 fn artifact_hash_depends_on_go_module() {
680 let h1 = compute_emit_artifact_hash(100, "github.com/old/proj");
681 let h2 = compute_emit_artifact_hash(100, "github.com/new/proj");
682 assert_ne!(h1, h2);
683 }
684
685 #[test]
686 fn apply_emit_stamps_round_trip() {
687 let tmp = tempfile::tempdir().unwrap();
688 let root = tmp.path();
689 std::fs::create_dir_all(root.join("target").join("cache")).unwrap();
690
691 let interface = ModuleInterface {
692 version: CACHE_FORMAT_VERSION,
693 compiler_version: COMPILER_VERSION_HASH,
694 stdlib_hash: STDLIB_HASH,
695 module_hash: 0,
696 source_hash: 100,
697 dependency_hashes: HashMap::default(),
698 files: vec![],
699 definitions: HashMap::default(),
700 ufcs_methods: vec![],
701 emit_stamp: None,
702 };
703 let path = cache_path(root, "greet");
704 std::fs::write(&path, bincode::serialize(&interface).unwrap()).unwrap();
705
706 let stamp = EmitStamp {
707 module_id: "greet".to_string(),
708 artifact_hash: 999,
709 };
710 apply_emit_stamps(root, &[(stamp.clone(), Some(999))]).unwrap();
711 let reread: ModuleInterface = bincode::deserialize(&std::fs::read(&path).unwrap()).unwrap();
712 assert_eq!(reread.emit_stamp, Some(999));
713 assert_eq!(reread.source_hash, 100);
714
715 apply_emit_stamps(root, &[(stamp, None)]).unwrap();
716 let reread: ModuleInterface = bincode::deserialize(&std::fs::read(&path).unwrap()).unwrap();
717 assert_eq!(reread.emit_stamp, None);
718 }
719
720 #[test]
721 fn apply_emit_stamps_missing_cache_is_no_op() {
722 let tmp = tempfile::tempdir().unwrap();
723 let stamp = EmitStamp {
724 module_id: "absent".to_string(),
725 artifact_hash: 0,
726 };
727 let result = apply_emit_stamps(tmp.path(), &[(stamp, None)]);
728 assert!(result.is_ok());
729 }
730
731 #[test]
732 fn try_load_cache_rejects_unstamped_for_emit() {
733 let tmp = tempfile::tempdir().unwrap();
734 let root = tmp.path();
735 std::fs::create_dir_all(root.join("target").join("cache")).unwrap();
736 std::fs::create_dir_all(root.join("target").join("greet")).unwrap();
737 std::fs::write(root.join("target").join("greet").join("greet.go"), "").unwrap();
738
739 let interface = ModuleInterface {
740 version: CACHE_FORMAT_VERSION,
741 compiler_version: COMPILER_VERSION_HASH,
742 stdlib_hash: STDLIB_HASH,
743 module_hash: 0,
744 source_hash: 100,
745 dependency_hashes: HashMap::default(),
746 files: vec![CachedFile {
747 name: "greet.lis".to_string(),
748 source: String::new(),
749 }],
750 definitions: HashMap::default(),
751 ufcs_methods: vec![],
752 emit_stamp: None,
753 };
754 let path = cache_path(root, "greet");
755 std::fs::write(&path, bincode::serialize(&interface).unwrap()).unwrap();
756
757 let loaded = try_load_cache("greet", 100, &HashMap::default(), None, root, false);
758 assert!(loaded.is_some(), "Check phase must accept unstamped cache");
759
760 let loaded = try_load_cache(
761 "greet",
762 100,
763 &HashMap::default(),
764 Some(compute_emit_artifact_hash(100, "github.com/test/x")),
765 root,
766 true,
767 );
768 assert!(
769 loaded.is_none(),
770 "Emit phase must reject cache with emit_stamp = None"
771 );
772 }
773
774 #[test]
775 fn try_load_cache_rejects_after_debug_invalidation() {
776 let tmp = tempfile::tempdir().unwrap();
777 let root = tmp.path();
778 std::fs::create_dir_all(root.join("target").join("cache")).unwrap();
779 std::fs::create_dir_all(root.join("target").join("greet")).unwrap();
780 std::fs::write(root.join("target").join("greet").join("greet.go"), "").unwrap();
781
782 let artifact_hash = compute_emit_artifact_hash(100, "github.com/test/x");
783
784 let interface = ModuleInterface {
785 version: CACHE_FORMAT_VERSION,
786 compiler_version: COMPILER_VERSION_HASH,
787 stdlib_hash: STDLIB_HASH,
788 module_hash: 0,
789 source_hash: 100,
790 dependency_hashes: HashMap::default(),
791 files: vec![CachedFile {
792 name: "greet.lis".to_string(),
793 source: String::new(),
794 }],
795 definitions: HashMap::default(),
796 ufcs_methods: vec![],
797 emit_stamp: Some(artifact_hash),
798 };
799 let path = cache_path(root, "greet");
800 std::fs::write(&path, bincode::serialize(&interface).unwrap()).unwrap();
801
802 assert!(
803 try_load_cache(
804 "greet",
805 100,
806 &HashMap::default(),
807 Some(artifact_hash),
808 root,
809 true,
810 )
811 .is_some()
812 );
813
814 let stamp = EmitStamp {
815 module_id: "greet".to_string(),
816 artifact_hash,
817 };
818 apply_emit_stamps(root, &[(stamp, None)]).unwrap();
819
820 assert!(
821 try_load_cache(
822 "greet",
823 100,
824 &HashMap::default(),
825 Some(artifact_hash),
826 root,
827 true,
828 )
829 .is_none()
830 );
831 }
832
833 #[test]
834 fn prune_legacy_global_caches_removes_only_hashed_files() {
835 let tmp = tempfile::tempdir().unwrap();
836 let dir = tmp.path();
837 let legacy_prelude = dir.join("prelude_defs_4330e9_compiler_f709f8.bin");
838 let legacy_stdlib = dir.join("stdlib_defs_151b6b_compiler_f709f8_darwin_arm64.bin");
839 let stable_prelude = dir.join("prelude_defs.bin");
840 let stable_stdlib = dir.join("stdlib_defs_darwin_arm64.bin");
841 let other_stdlib = dir.join("stdlib_defs_linux_amd64.bin");
842 for path in [
843 &legacy_prelude,
844 &legacy_stdlib,
845 &stable_prelude,
846 &stable_stdlib,
847 &other_stdlib,
848 ] {
849 std::fs::write(path, b"x").unwrap();
850 }
851
852 prune_legacy_global_caches(dir, "prelude_defs");
853 prune_legacy_global_caches(dir, "stdlib_defs");
854
855 assert!(!legacy_prelude.exists());
856 assert!(!legacy_stdlib.exists());
857 assert!(stable_prelude.exists());
858 assert!(stable_stdlib.exists());
859 assert!(other_stdlib.exists());
860 }
861
862 #[test]
863 fn prune_legacy_global_caches_missing_dir_is_noop() {
864 let tmp = tempfile::tempdir().unwrap();
865 prune_legacy_global_caches(&tmp.path().join("does_not_exist"), "prelude_defs");
866 }
867
868 #[test]
869 fn global_cache_temp_paths_are_unique() {
870 let base = Path::new("/cache/prelude_defs.bin");
871 let first = global_cache_temp_path(base);
872 let second = global_cache_temp_path(base);
873 assert_ne!(first, second);
874 }
875}