1use std::borrow::Cow;
45use std::fs;
46use std::io::{self, Read as _};
47use std::path::{Path, PathBuf};
48use std::sync::Arc;
49
50use serde::{de::DeserializeOwned, Serialize};
51use sha2::{Digest, Sha256};
52
53use crate::chunk::{CachedChunk, Chunk};
54use crate::compiler::CompilerOptions;
55use crate::context_manifest::{
56 ContextManifest, GraphLinkTable, ManifestCheck, ManifestFile, ManifestUnreadable,
57 ManifestUnresolved,
58};
59use crate::module_artifact::{ModuleArtifact, ModuleCompilationContext};
60use crate::module_source::{self, ModuleSource};
61
62pub const MAGIC: &[u8; 8] = b"HARNBC\0\0";
64
65pub const SCHEMA_VERSION: u32 = 11;
86
87pub const HARN_VERSION: &str = env!("CARGO_PKG_VERSION");
90
91pub const CODEGEN_FINGERPRINT: &str = env!("HARN_CODEGEN_FINGERPRINT");
106
107pub const CACHE_EXTENSION: &str = "harnbc";
109
110pub const MODULE_CACHE_EXTENSION: &str = "harnmod";
115
116const KIND_ENTRY_CHUNK: u8 = 1;
118const KIND_MODULE_ARTIFACT: u8 = 2;
120
121pub const CACHE_DIR_ENV: &str = "HARN_CACHE_DIR";
124
125pub const CACHE_ENABLED_ENV: &str = "HARN_BYTECODE_CACHE";
129
130pub struct LookupOutcome {
133 pub key: CacheKey,
134 pub chunk: Option<Chunk>,
135 pub manifest: Option<ContextManifest>,
139 pub link_table: Option<Arc<GraphLinkTable>>,
148}
149
150impl LookupOutcome {
151 pub fn store(&self, chunk: &Chunk) -> io::Result<()> {
159 store(&self.key, chunk, self.manifest.as_ref())
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
166pub struct CacheKey {
167 pub source_hash: [u8; 32],
168 pub context_hash: [u8; 32],
169 pub harn_version: Cow<'static, str>,
178 pub compiler_tag: u8,
182}
183
184impl CacheKey {
185 pub fn from_source(source_path: &Path, source: &str) -> Self {
189 let source_hash = sha256(source.as_bytes());
190 let context_hash = hash_transitive_user_imports(source_path, source);
191 Self {
192 source_hash,
193 context_hash,
194 harn_version: Cow::Borrowed(HARN_VERSION),
195 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
196 }
197 }
198
199 pub fn from_relocatable_source(source_path: &Path, source: &str) -> Self {
208 let source_hash = sha256(source.as_bytes());
209 let context_hash = hash_relocatable_user_imports(source_path, source);
210 Self {
211 source_hash,
212 context_hash,
213 harn_version: Cow::Borrowed(HARN_VERSION),
214 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
215 }
216 }
217
218 #[must_use]
222 pub fn for_artifact_version(mut self, harn_version: impl Into<String>) -> Self {
223 self.harn_version = Cow::Owned(harn_version.into());
224 self
225 }
226
227 pub fn from_module_source(
236 source: &ModuleSource,
237 compilation_context: &ModuleCompilationContext,
238 ) -> Self {
239 Self::from_module_content_hash(source.sha256(), compilation_context)
240 }
241
242 pub fn from_module_content_hash(
249 content_hash: [u8; 32],
250 compilation_context: &ModuleCompilationContext,
251 ) -> Self {
252 Self {
253 source_hash: content_hash,
254 context_hash: module_compilation_context_hash(compilation_context),
255 harn_version: Cow::Borrowed(HARN_VERSION),
256 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
257 }
258 }
259
260 pub fn filename(&self) -> String {
265 format!("{}.{}", hex(&self.source_hash), CACHE_EXTENSION)
266 }
267
268 pub fn module_filename(&self) -> String {
272 let mut hasher = Sha256::new();
273 hasher.update(self.source_hash);
274 hasher.update(self.context_hash);
275 hasher.update(self.harn_version.as_bytes());
276 hasher.update([self.compiler_tag]);
277 let identity: [u8; 32] = hasher.finalize().into();
278 format!("{}.{}", hex(&identity), MODULE_CACHE_EXTENSION)
279 }
280}
281
282pub fn cache_dir() -> PathBuf {
288 if let Some(custom) = std::env::var_os(CACHE_DIR_ENV) {
289 return PathBuf::from(custom);
290 }
291 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
292 let xdg = PathBuf::from(xdg);
293 if !xdg.as_os_str().is_empty() {
294 return xdg.join("harn").join("bytecode");
295 }
296 }
297 if let Some(home) = crate::user_dirs::home_dir() {
298 return home.join(".cache").join("harn").join("bytecode");
299 }
300 PathBuf::from(".harn-cache").join("bytecode")
303}
304
305pub fn packs_cache_dir() -> PathBuf {
310 if let Some(custom) = std::env::var_os(CACHE_DIR_ENV) {
311 return PathBuf::from(custom).join("packs");
312 }
313 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
314 let xdg = PathBuf::from(xdg);
315 if !xdg.as_os_str().is_empty() {
316 return xdg.join("harn").join("packs");
317 }
318 }
319 if let Some(home) = crate::user_dirs::home_dir() {
320 return home.join(".cache").join("harn").join("packs");
321 }
322 PathBuf::from(".harn-cache").join("packs")
323}
324
325pub fn cache_enabled() -> bool {
327 match std::env::var(CACHE_ENABLED_ENV).ok().as_deref() {
328 Some(value) => !matches!(
329 value.to_ascii_lowercase().as_str(),
330 "0" | "false" | "no" | "off"
331 ),
332 None => true,
333 }
334}
335
336pub fn load(source_path: &Path, source: &str) -> LookupOutcome {
340 let mut key = CacheKey {
344 source_hash: sha256(source.as_bytes()),
345 context_hash: [0u8; 32],
346 harn_version: Cow::Borrowed(HARN_VERSION),
347 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
348 };
349 let mut walk = GraphWalk::new(source_path, source);
350
351 if !cache_enabled() {
352 let (context_hash, manifest) = walk.finish();
353 key.context_hash = context_hash;
354 return LookupOutcome {
355 key,
356 chunk: None,
357 manifest,
358 link_table: None,
359 };
360 }
361
362 let mut candidates: Vec<(PathBuf, bool)> = Vec::with_capacity(2);
363 if let Some(adjacent) = adjacent_cache_path(source_path) {
364 candidates.push((adjacent, true));
365 }
366 candidates.push((cache_dir().join(key.filename()), false));
367
368 let entry = module_source::canonical_identity(source_path);
373
374 for (path, allow_relocatable) in candidates {
375 let Ok(Some(candidate)) = read_entry_candidate(&path, &key) else {
376 continue;
377 };
378 match candidate
379 .manifest
380 .as_ref()
381 .map(|manifest| manifest.check(&entry))
382 {
383 Some(ManifestCheck::Valid) => {
386 key.context_hash = candidate.context_hash;
387 return LookupOutcome {
388 key,
389 chunk: Some(candidate.chunk),
390 link_table: candidate.manifest.as_ref().map(link_table_for),
391 manifest: candidate.manifest,
392 };
393 }
394 Some(ManifestCheck::ValidAfterRecheck { refreshed }) => {
399 key.context_hash = candidate.context_hash;
400 let _ = write_atomic_chunk(&path, &key, &candidate.chunk, Some(&refreshed));
401 return LookupOutcome {
402 key,
403 chunk: Some(candidate.chunk),
404 link_table: Some(link_table_for(&refreshed)),
405 manifest: Some(refreshed),
406 };
407 }
408 Some(ManifestCheck::Stale) | None => {}
409 }
410 if walk.context_hash() != candidate.context_hash {
411 if !allow_relocatable || walk.relocatable_context_hash() != candidate.context_hash {
412 continue;
413 }
414 key.context_hash = candidate.context_hash;
418 return LookupOutcome {
419 key,
420 chunk: Some(candidate.chunk),
421 manifest: walk.manifest().cloned(),
422 link_table: None,
423 };
424 }
425 key.context_hash = candidate.context_hash;
429 let manifest = walk.manifest().cloned();
430 let _ = write_atomic_chunk(&path, &key, &candidate.chunk, manifest.as_ref());
431 return LookupOutcome {
432 key,
433 chunk: Some(candidate.chunk),
434 manifest,
435 link_table: None,
436 };
437 }
438
439 let (context_hash, manifest) = walk.finish();
440 key.context_hash = context_hash;
441 LookupOutcome {
442 key,
443 chunk: None,
444 manifest,
445 link_table: None,
446 }
447}
448
449fn link_table_for(manifest: &ContextManifest) -> Arc<GraphLinkTable> {
453 Arc::new(GraphLinkTable::from_validated(manifest))
454}
455
456struct GraphWalk<'a> {
459 source_path: &'a Path,
460 source: &'a str,
461 result: Option<GraphHashes>,
462}
463
464impl<'a> GraphWalk<'a> {
465 fn new(source_path: &'a Path, source: &'a str) -> Self {
466 Self {
467 source_path,
468 source,
469 result: None,
470 }
471 }
472
473 fn run(&mut self) -> &GraphHashes {
474 self.result.get_or_insert_with(|| {
475 walk_import_graph_fingerprinted(self.source_path, self.source, CODEGEN_FINGERPRINT)
476 })
477 }
478
479 fn context_hash(&mut self) -> [u8; 32] {
480 self.run().canonical
481 }
482
483 fn relocatable_context_hash(&mut self) -> [u8; 32] {
484 self.run().relocatable
485 }
486
487 fn manifest(&mut self) -> Option<&ContextManifest> {
488 self.run().manifest.as_ref()
489 }
490
491 fn finish(mut self) -> ([u8; 32], Option<ContextManifest>) {
492 self.run();
493 let result = self.result.expect("the walk was just run");
494 (result.canonical, result.manifest)
495 }
496}
497
498pub fn store(key: &CacheKey, chunk: &Chunk, manifest: Option<&ContextManifest>) -> io::Result<()> {
502 if !cache_enabled() {
503 return Ok(());
504 }
505 let dir = cache_dir();
506 fs::create_dir_all(&dir)?;
507 write_atomic_chunk(&dir.join(key.filename()), key, chunk, manifest)
508}
509
510pub fn store_at(path: &Path, key: &CacheKey, chunk: &Chunk) -> io::Result<()> {
515 ensure_parent_dir(path)?;
516 write_atomic_chunk(path, key, chunk, None)
517}
518
519pub fn load_module(
522 source_path: &Path,
523 source: &ModuleSource,
524 compilation_context: &ModuleCompilationContext,
525) -> ModuleLookupOutcome {
526 load_module_for_key(
527 source_path,
528 CacheKey::from_module_source(source, compilation_context),
529 )
530}
531
532pub fn load_module_for_key(source_path: &Path, key: CacheKey) -> ModuleLookupOutcome {
539 if !cache_enabled() {
540 return ModuleLookupOutcome {
541 key,
542 artifact: None,
543 };
544 }
545 let mut candidates: Vec<PathBuf> = Vec::with_capacity(2);
546 if let Some(adjacent) = adjacent_module_cache_path(source_path) {
547 candidates.push(adjacent);
548 }
549 candidates.push(cache_dir().join(key.module_filename()));
550 for path in candidates {
551 match read_module_if_matches(&path, &key, source_path) {
552 Ok(Some(artifact)) => {
553 return ModuleLookupOutcome {
554 key,
555 artifact: Some(artifact),
556 }
557 }
558 Ok(None) => continue,
559 Err(_) => continue,
560 }
561 }
562 ModuleLookupOutcome {
563 key,
564 artifact: None,
565 }
566}
567
568pub fn store_module(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
571 if !cache_enabled() {
572 return Ok(());
573 }
574 let dir = cache_dir();
575 fs::create_dir_all(&dir)?;
576 write_atomic_module(&dir.join(key.module_filename()), key, artifact)
577}
578
579pub fn store_module_at(path: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
581 ensure_parent_dir(path)?;
582 write_atomic_module(path, key, artifact)
583}
584
585pub struct ModuleLookupOutcome {
588 pub key: CacheKey,
589 pub artifact: Option<ModuleArtifact>,
590}
591
592pub fn adjacent_cache_path(source_path: &Path) -> Option<PathBuf> {
595 adjacent_path_with_extension(source_path, CACHE_EXTENSION)
596}
597
598pub fn adjacent_module_cache_path(source_path: &Path) -> Option<PathBuf> {
601 adjacent_path_with_extension(source_path, MODULE_CACHE_EXTENSION)
602}
603
604fn adjacent_path_with_extension(source_path: &Path, ext: &str) -> Option<PathBuf> {
605 let stem = source_path.file_stem()?;
606 if stem.is_empty() {
607 return None;
608 }
609 let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
610 let mut out = parent.join(stem);
611 out.set_extension(ext);
612 Some(out)
613}
614
615fn ensure_parent_dir(path: &Path) -> io::Result<()> {
616 if let Some(parent) = path.parent() {
617 if !parent.as_os_str().is_empty() {
618 fs::create_dir_all(parent)?;
619 }
620 }
621 Ok(())
622}
623
624fn write_atomic_chunk(
625 target: &Path,
626 key: &CacheKey,
627 chunk: &Chunk,
628 manifest: Option<&ContextManifest>,
629) -> io::Result<()> {
630 let buf = serialize_chunk_artifact_with_manifest(key, chunk, manifest)?;
631 crate::atomic_io::atomic_write(target, &buf)
632}
633
634fn write_atomic_module(target: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
635 let buf = serialize_module_artifact(key, artifact)?;
636 crate::atomic_io::atomic_write(target, &buf)
637}
638
639pub fn serialize_chunk_artifact(key: &CacheKey, chunk: &Chunk) -> io::Result<Vec<u8>> {
645 serialize_chunk_artifact_with_manifest(key, chunk, None)
646}
647
648pub fn serialize_chunk_artifact_with_manifest(
656 key: &CacheKey,
657 chunk: &Chunk,
658 manifest: Option<&ContextManifest>,
659) -> io::Result<Vec<u8>> {
660 let payload = serialize_cache_payload(&EntryPayload {
661 manifest: manifest.cloned(),
662 chunk: chunk.freeze_for_cache(),
663 })?;
664 Ok(encode_artifact(key, KIND_ENTRY_CHUNK, &payload))
665}
666
667pub fn serialize_module_artifact(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<Vec<u8>> {
670 let payload = serialize_cache_payload(artifact)?;
671 Ok(encode_artifact(key, KIND_MODULE_ARTIFACT, &payload))
672}
673
674#[derive(serde::Serialize, serde::Deserialize)]
678struct EntryPayload {
679 manifest: Option<ContextManifest>,
680 chunk: CachedChunk,
681}
682
683fn serialize_cache_payload<T: Serialize>(value: &T) -> io::Result<Vec<u8>> {
684 postcard::to_allocvec(value)
685 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
686}
687
688fn deserialize_cache_payload<T: DeserializeOwned>(payload: &[u8]) -> Result<T, String> {
689 let (value, remaining) = postcard::take_from_bytes(payload).map_err(|err| err.to_string())?;
690 if remaining.is_empty() {
691 Ok(value)
692 } else {
693 Err("cache payload contains trailing bytes".to_string())
694 }
695}
696
697fn encode_artifact(key: &CacheKey, kind: u8, payload: &[u8]) -> Vec<u8> {
698 encode_artifact_fingerprinted(key, kind, payload, CODEGEN_FINGERPRINT)
699}
700
701fn encode_artifact_fingerprinted(
705 key: &CacheKey,
706 kind: u8,
707 payload: &[u8],
708 codegen_fingerprint: &str,
709) -> Vec<u8> {
710 let mut buf: Vec<u8> = Vec::with_capacity(payload.len() + 128);
711 buf.extend_from_slice(MAGIC);
712 buf.extend_from_slice(&SCHEMA_VERSION.to_le_bytes());
713 let version_bytes = key.harn_version.as_bytes();
714 buf.extend_from_slice(&(version_bytes.len() as u32).to_le_bytes());
715 buf.extend_from_slice(version_bytes);
716 let fingerprint_bytes = codegen_fingerprint.as_bytes();
717 buf.extend_from_slice(&(fingerprint_bytes.len() as u32).to_le_bytes());
718 buf.extend_from_slice(fingerprint_bytes);
719 buf.push(key.compiler_tag);
720 buf.push(kind);
721 buf.extend_from_slice(&key.source_hash);
722 buf.extend_from_slice(&key.context_hash);
723 buf.extend_from_slice(payload);
724 buf
725}
726
727fn read_length_prefixed_match(file: &mut fs::File, len: usize, expected: &[u8]) -> bool {
733 if len > 256 || len != expected.len() {
734 return false;
735 }
736 let mut buf = vec![0u8; len];
737 file.read_exact(&mut buf).is_ok() && buf == expected
738}
739
740struct ParsedHeader {
743 kind: u8,
744 context_hash: [u8; 32],
745 payload: Vec<u8>,
746}
747
748fn read_header_if_matches(
754 path: &Path,
755 key: &CacheKey,
756 expected_context: Option<&[u8; 32]>,
757) -> io::Result<Option<ParsedHeader>> {
758 let mut file = match fs::File::open(path) {
759 Ok(f) => f,
760 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
761 Err(err) => return Err(err),
762 };
763 let mut header = [0u8; 8 + 4 + 4];
764 if file.read_exact(&mut header).is_err() {
765 return Ok(None);
766 }
767 if &header[..8] != MAGIC {
768 return Ok(None);
769 }
770 let schema = u32::from_le_bytes(header[8..12].try_into().unwrap());
771 if schema != SCHEMA_VERSION {
772 return Ok(None);
773 }
774 let version_len = u32::from_le_bytes(header[12..16].try_into().unwrap()) as usize;
775 if !read_length_prefixed_match(&mut file, version_len, key.harn_version.as_bytes()) {
776 return Ok(None);
777 }
778 let mut fingerprint_len_bytes = [0u8; 4];
784 if file.read_exact(&mut fingerprint_len_bytes).is_err() {
785 return Ok(None);
786 }
787 let fingerprint_len = u32::from_le_bytes(fingerprint_len_bytes) as usize;
788 if !read_length_prefixed_match(&mut file, fingerprint_len, CODEGEN_FINGERPRINT.as_bytes()) {
789 return Ok(None);
790 }
791 let mut compiler_and_kind = [0u8; 2];
792 if file.read_exact(&mut compiler_and_kind).is_err() {
793 return Ok(None);
794 }
795 if compiler_and_kind[0] != key.compiler_tag {
796 return Ok(None);
797 }
798 let kind = compiler_and_kind[1];
799 let mut hashes = [0u8; 64];
800 if file.read_exact(&mut hashes).is_err() {
801 return Ok(None);
802 }
803 if hashes[..32] != key.source_hash {
804 return Ok(None);
805 }
806 let mut context_hash = [0u8; 32];
807 context_hash.copy_from_slice(&hashes[32..]);
808 if expected_context.is_some_and(|expected| *expected != context_hash) {
809 return Ok(None);
810 }
811 let mut payload = Vec::new();
812 if file.read_to_end(&mut payload).is_err() {
813 return Ok(None);
814 }
815 Ok(Some(ParsedHeader {
816 kind,
817 context_hash,
818 payload,
819 }))
820}
821
822struct CandidateEntry {
825 context_hash: [u8; 32],
826 manifest: Option<ContextManifest>,
827 chunk: Chunk,
828}
829
830fn read_entry_candidate(path: &Path, key: &CacheKey) -> io::Result<Option<CandidateEntry>> {
831 let Some(header) = read_header_if_matches(path, key, None)? else {
832 return Ok(None);
833 };
834 if header.kind != KIND_ENTRY_CHUNK {
835 return Ok(None);
836 }
837 let payload: EntryPayload = match deserialize_cache_payload(&header.payload) {
838 Ok(p) => p,
839 Err(_) => return Ok(None),
840 };
841 Ok(Some(CandidateEntry {
842 context_hash: header.context_hash,
843 manifest: payload.manifest,
844 chunk: Chunk::from_cached(payload.chunk),
845 }))
846}
847
848fn read_module_if_matches(
849 path: &Path,
850 key: &CacheKey,
851 source_path: &Path,
852) -> io::Result<Option<ModuleArtifact>> {
853 let Some(header) = read_header_if_matches(path, key, Some(&key.context_hash))? else {
854 return Ok(None);
855 };
856 if header.kind != KIND_MODULE_ARTIFACT {
857 return Ok(None);
858 }
859 match deserialize_cache_payload::<ModuleArtifact>(&header.payload) {
860 Ok(mut artifact) => {
861 artifact.bind_source_file(source_path);
862 Ok(Some(artifact))
863 }
864 Err(_) => Ok(None),
865 }
866}
867
868fn compiler_options_tag(options: CompilerOptions) -> u8 {
875 let mut tag: u8 = 0;
876 if options.optimizations_enabled() {
877 tag |= 0b0000_0001;
878 }
879 if options.legacy_ambient_capabilities() {
880 tag |= 0b0000_0010;
881 }
882 tag
883}
884
885fn sha256(bytes: &[u8]) -> [u8; 32] {
886 let mut hasher = Sha256::new();
887 hasher.update(bytes);
888 hasher.finalize().into()
889}
890
891fn hex(bytes: &[u8]) -> String {
892 let mut out = String::with_capacity(bytes.len() * 2);
893 for byte in bytes {
894 out.push_str(&format!("{byte:02x}"));
895 }
896 out
897}
898
899fn embedded_stdlib_digest() -> &'static [u8; 32] {
910 use std::sync::OnceLock;
911 static DIGEST: OnceLock<[u8; 32]> = OnceLock::new();
912 DIGEST.get_or_init(|| {
913 let mut entries: Vec<(&'static str, &'static str)> = harn_stdlib::STDLIB_SOURCES
914 .iter()
915 .map(|src| (src.module, src.source))
916 .collect();
917 entries.sort_by(|a, b| a.0.cmp(b.0));
918 let mut hasher = Sha256::new();
919 for (module, source) in entries {
920 hasher.update(module.as_bytes());
921 hasher.update(b"\0");
922 hasher.update(source.as_bytes());
923 hasher.update(b"\0");
924 }
925 hasher.finalize().into()
926 })
927}
928
929fn module_compilation_context_hash(compilation_context: &ModuleCompilationContext) -> [u8; 32] {
936 module_compilation_context_hash_fingerprinted(CODEGEN_FINGERPRINT, compilation_context.digest())
937}
938
939fn module_compilation_context_hash_fingerprinted(
940 codegen_fingerprint: &str,
941 imported_interface_digest: [u8; 32],
942) -> [u8; 32] {
943 let mut hasher = Sha256::new();
944 hasher.update(b"module-artifact-source-local-v4\0");
945 hasher.update(b"stdlib-digest\0");
946 hasher.update(embedded_stdlib_digest());
947 hasher.update(b"\0codegen-fingerprint\0");
948 hasher.update(codegen_fingerprint.as_bytes());
949 hasher.update(b"\0imported-interface\0");
950 hasher.update(imported_interface_digest);
951 hasher.finalize().into()
952}
953
954#[cfg(test)]
962thread_local! {
963 pub(crate) static WALKS_PERFORMED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
964}
965
966fn hash_transitive_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
977 hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT).0
978}
979
980fn hash_relocatable_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
983 walk_import_graph_fingerprinted(source_path, source, CODEGEN_FINGERPRINT).relocatable
984}
985
986#[cfg(test)]
989fn hash_transitive_user_imports_with_manifest(
990 source_path: &Path,
991 source: &str,
992) -> ([u8; 32], Option<ContextManifest>) {
993 hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT)
994}
995
996fn hash_transitive_user_imports_fingerprinted(
1000 source_path: &Path,
1001 source: &str,
1002 codegen_fingerprint: &str,
1003) -> ([u8; 32], Option<ContextManifest>) {
1004 let result = walk_import_graph_fingerprinted(source_path, source, codegen_fingerprint);
1005 (result.canonical, result.manifest)
1006}
1007
1008struct GraphHashes {
1009 canonical: [u8; 32],
1010 relocatable: [u8; 32],
1011 manifest: Option<ContextManifest>,
1012}
1013
1014fn walk_import_graph_fingerprinted(
1015 source_path: &Path,
1016 source: &str,
1017 codegen_fingerprint: &str,
1018) -> GraphHashes {
1019 #[cfg(test)]
1020 WALKS_PERFORMED.with(|c| c.set(c.get() + 1));
1021
1022 let mut visited: std::collections::BTreeMap<PathBuf, ImportNode> =
1023 std::collections::BTreeMap::new();
1024 let entry = ModuleSource::from_text(source);
1025 let mut frontier: Vec<(PathBuf, Arc<str>)> = entry
1026 .imports()
1027 .iter()
1028 .map(|import| (source_path.to_path_buf(), Arc::clone(import)))
1029 .collect();
1030 let mut manifest = Some(ContextManifest::begin(module_source::canonical_identity(
1037 source_path,
1038 )));
1039
1040 while let Some((anchor, import)) = frontier.pop() {
1041 let Some(resolved) = harn_modules::resolve_import_path(&anchor, &import) else {
1042 let sentinel = anchor.join(format!("__unresolved__/{import}"));
1046 if let std::collections::btree_map::Entry::Vacant(slot) = visited.entry(sentinel) {
1047 slot.insert(ImportNode::Unresolved {
1048 import: Arc::clone(&import),
1049 });
1050 if let Some(m) = manifest.as_mut() {
1051 m.unresolved.push(ManifestUnresolved {
1052 anchor: anchor.clone(),
1053 import: import.to_string(),
1054 });
1055 }
1056 }
1057 continue;
1058 };
1059 let canonical = module_source::canonical_identity(&resolved);
1060 if visited.contains_key(&canonical) {
1061 continue;
1062 }
1063 match module_source::read(&resolved) {
1070 Ok(module) => {
1071 visited.insert(
1072 canonical.clone(),
1073 ImportNode::Resolved {
1074 content: Arc::clone(module.text()),
1075 },
1076 );
1077 match ManifestFile::observe(&canonical, &module) {
1078 Some(file) => {
1079 if let Some(m) = manifest.as_mut() {
1080 m.files.push(file);
1081 }
1082 }
1083 None => manifest = None,
1087 }
1088 for nested_import in module.imports() {
1089 frontier.push((resolved.clone(), Arc::clone(nested_import)));
1090 }
1091 }
1092 Err(error) => {
1093 let unreadable_path = canonical.clone();
1094 visited.insert(
1095 canonical,
1096 ImportNode::IoError {
1097 kind: error.kind().to_string(),
1098 },
1099 );
1100 if let Some(m) = manifest.as_mut() {
1105 m.unreadable.push(ManifestUnreadable {
1106 path: unreadable_path,
1107 kind: error.kind().to_string(),
1108 });
1109 }
1110 }
1111 }
1112 }
1113
1114 if let Some(recorded) = manifest.as_ref() {
1119 let graph = harn_modules::build_with_source(source_path, source);
1120 let contexts = recorded
1121 .files
1122 .iter()
1123 .map(|file| match visited.get(&file.path) {
1124 Some(ImportNode::Resolved { content }) => {
1125 ModuleCompilationContext::for_source_in_graph(
1126 &graph,
1127 &file.path,
1128 content.as_ref(),
1129 )
1130 .ok()
1131 }
1132 _ => None,
1133 })
1134 .collect::<Option<Vec<_>>>();
1135 if let Some(contexts) = contexts {
1136 for (file, context) in manifest
1137 .as_mut()
1138 .expect("the manifest was just borrowed")
1139 .files
1140 .iter_mut()
1141 .zip(contexts)
1142 {
1143 file.compilation_context = context;
1144 }
1145 } else {
1146 manifest = None;
1150 }
1151 }
1152
1153 let mut canonical_hasher = Sha256::new();
1154 seed_entry_context_hasher(&mut canonical_hasher, codegen_fingerprint);
1155 let mut relocatable_hasher = Sha256::new();
1156 relocatable_hasher.update(b"relocatable-entry-graph-v1\0");
1157 seed_entry_context_hasher(&mut relocatable_hasher, codegen_fingerprint);
1158
1159 let entry_identity = module_source::canonical_identity(source_path);
1160 let entry_dir = entry_identity.parent().unwrap_or(Path::new(""));
1161 let mut relocatable_nodes = Vec::with_capacity(visited.len());
1162 for (path, node) in &visited {
1163 canonical_hasher.update(path.to_string_lossy().as_bytes());
1164 canonical_hasher.update(b"\0");
1165 hash_import_node(&mut canonical_hasher, node);
1166 canonical_hasher.update(b"\0");
1167
1168 let Some(label) = relative_path_label(entry_dir, path) else {
1169 relocatable_nodes.push((path.to_string_lossy().replace('\\', "/"), node));
1173 continue;
1174 };
1175 relocatable_nodes.push((label, node));
1176 }
1177 relocatable_nodes.sort_by(|left, right| left.0.cmp(&right.0));
1178 for (path, node) in relocatable_nodes {
1179 relocatable_hasher.update(path.as_bytes());
1180 relocatable_hasher.update(b"\0");
1181 hash_import_node(&mut relocatable_hasher, node);
1182 relocatable_hasher.update(b"\0");
1183 }
1184
1185 if let Some(m) = manifest.as_mut() {
1188 m.files.sort_by(|a, b| a.path.cmp(&b.path));
1189 m.unresolved
1190 .sort_by(|a, b| (&a.anchor, &a.import).cmp(&(&b.anchor, &b.import)));
1191 m.unreadable.sort_by(|a, b| a.path.cmp(&b.path));
1192 }
1193 GraphHashes {
1194 canonical: canonical_hasher.finalize().into(),
1195 relocatable: relocatable_hasher.finalize().into(),
1196 manifest,
1197 }
1198}
1199
1200fn seed_entry_context_hasher(hasher: &mut Sha256, codegen_fingerprint: &str) {
1201 hasher.update(b"stdlib-digest\0");
1202 hasher.update(embedded_stdlib_digest());
1203 hasher.update(b"\0");
1204 hasher.update(b"codegen-fingerprint\0");
1209 hasher.update(codegen_fingerprint.as_bytes());
1210 hasher.update(b"\0");
1211}
1212
1213fn hash_import_node(hasher: &mut Sha256, node: &ImportNode) {
1214 match node {
1215 ImportNode::Resolved { content } => {
1216 hasher.update(b"resolved\0");
1217 hasher.update(content.as_bytes());
1218 }
1219 ImportNode::Unresolved { import } => {
1220 hasher.update(b"unresolved\0");
1221 hasher.update(import.as_bytes());
1222 }
1223 ImportNode::IoError { kind } => {
1224 hasher.update(b"ioerror\0");
1225 hasher.update(kind.as_bytes());
1226 }
1227 }
1228}
1229
1230fn relative_path_label(base: &Path, target: &Path) -> Option<String> {
1234 let base_components = base.components().collect::<Vec<_>>();
1235 let target_components = target.components().collect::<Vec<_>>();
1236 let common = base_components
1237 .iter()
1238 .zip(&target_components)
1239 .take_while(|(left, right)| left == right)
1240 .count();
1241 if common == 0 && (base.is_absolute() || target.is_absolute()) {
1242 return None;
1243 }
1244
1245 let mut parts = Vec::new();
1246 for component in &base_components[common..] {
1247 if matches!(component, std::path::Component::Normal(_)) {
1248 parts.push("..".to_string());
1249 }
1250 }
1251 for component in &target_components[common..] {
1252 match component {
1253 std::path::Component::Normal(part) => parts.push(part.to_string_lossy().into_owned()),
1254 std::path::Component::ParentDir => parts.push("..".to_string()),
1255 std::path::Component::CurDir => {}
1256 std::path::Component::RootDir | std::path::Component::Prefix(_) => return None,
1257 }
1258 }
1259 Some(if parts.is_empty() {
1260 ".".to_string()
1261 } else {
1262 parts.join("/")
1263 })
1264}
1265
1266enum ImportNode {
1267 Resolved { content: Arc<str> },
1268 Unresolved { import: Arc<str> },
1269 IoError { kind: String },
1270}
1271
1272#[cfg(test)]
1273#[path = "bytecode_cache_tests.rs"]
1274mod tests;