1use std::borrow::Cow;
44use std::fs;
45use std::io::{self, Read as _};
46use std::path::{Path, PathBuf};
47use std::sync::Arc;
48
49use serde::{de::DeserializeOwned, Serialize};
50use sha2::{Digest, Sha256};
51
52use crate::chunk::{CachedChunk, Chunk};
53use crate::compiler::CompilerOptions;
54use crate::context_manifest::{
55 ContextManifest, GraphLinkTable, ManifestCheck, ManifestFile, ManifestUnreadable,
56 ManifestUnresolved,
57};
58use crate::module_artifact::ModuleArtifact;
59use crate::module_source::{self, ModuleSource};
60
61pub const MAGIC: &[u8; 8] = b"HARNBC\0\0";
63
64pub const SCHEMA_VERSION: u32 = 10;
83
84pub const HARN_VERSION: &str = env!("CARGO_PKG_VERSION");
87
88pub const CODEGEN_FINGERPRINT: &str = env!("HARN_CODEGEN_FINGERPRINT");
103
104pub const CACHE_EXTENSION: &str = "harnbc";
106
107pub const MODULE_CACHE_EXTENSION: &str = "harnmod";
112
113const KIND_ENTRY_CHUNK: u8 = 1;
115const KIND_MODULE_ARTIFACT: u8 = 2;
117
118pub const CACHE_DIR_ENV: &str = "HARN_CACHE_DIR";
121
122pub const CACHE_ENABLED_ENV: &str = "HARN_BYTECODE_CACHE";
126
127pub struct LookupOutcome {
130 pub key: CacheKey,
131 pub chunk: Option<Chunk>,
132 pub manifest: Option<ContextManifest>,
136 pub link_table: Option<Arc<GraphLinkTable>>,
145}
146
147impl LookupOutcome {
148 pub fn store(&self, chunk: &Chunk) -> io::Result<()> {
156 store(&self.key, chunk, self.manifest.as_ref())
157 }
158}
159
160#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct CacheKey {
164 pub source_hash: [u8; 32],
165 pub context_hash: [u8; 32],
166 pub harn_version: Cow<'static, str>,
175 pub compiler_tag: u8,
179}
180
181impl CacheKey {
182 pub fn from_source(source_path: &Path, source: &str) -> Self {
186 let source_hash = sha256(source.as_bytes());
187 let context_hash = hash_transitive_user_imports(source_path, source);
188 Self {
189 source_hash,
190 context_hash,
191 harn_version: Cow::Borrowed(HARN_VERSION),
192 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
193 }
194 }
195
196 pub fn from_relocatable_source(source_path: &Path, source: &str) -> Self {
205 let source_hash = sha256(source.as_bytes());
206 let context_hash = hash_relocatable_user_imports(source_path, source);
207 Self {
208 source_hash,
209 context_hash,
210 harn_version: Cow::Borrowed(HARN_VERSION),
211 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
212 }
213 }
214
215 #[must_use]
219 pub fn for_artifact_version(mut self, harn_version: impl Into<String>) -> Self {
220 self.harn_version = Cow::Owned(harn_version.into());
221 self
222 }
223
224 pub fn from_module_source(source: &ModuleSource) -> Self {
233 Self::from_module_content_hash(source.sha256())
234 }
235
236 pub fn from_module_content_hash(content_hash: [u8; 32]) -> Self {
244 Self {
245 source_hash: content_hash,
246 context_hash: module_compilation_context_hash(),
247 harn_version: Cow::Borrowed(HARN_VERSION),
248 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
249 }
250 }
251
252 pub fn filename(&self) -> String {
257 format!("{}.{}", hex(&self.source_hash), CACHE_EXTENSION)
258 }
259
260 pub fn module_filename(&self) -> String {
264 let mut hasher = Sha256::new();
265 hasher.update(self.source_hash);
266 hasher.update(self.context_hash);
267 hasher.update(self.harn_version.as_bytes());
268 hasher.update([self.compiler_tag]);
269 let identity: [u8; 32] = hasher.finalize().into();
270 format!("{}.{}", hex(&identity), MODULE_CACHE_EXTENSION)
271 }
272}
273
274pub fn cache_dir() -> PathBuf {
280 if let Some(custom) = std::env::var_os(CACHE_DIR_ENV) {
281 return PathBuf::from(custom);
282 }
283 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
284 let xdg = PathBuf::from(xdg);
285 if !xdg.as_os_str().is_empty() {
286 return xdg.join("harn").join("bytecode");
287 }
288 }
289 if let Some(home) = crate::user_dirs::home_dir() {
290 return home.join(".cache").join("harn").join("bytecode");
291 }
292 PathBuf::from(".harn-cache").join("bytecode")
295}
296
297pub fn packs_cache_dir() -> PathBuf {
302 if let Some(custom) = std::env::var_os(CACHE_DIR_ENV) {
303 return PathBuf::from(custom).join("packs");
304 }
305 if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
306 let xdg = PathBuf::from(xdg);
307 if !xdg.as_os_str().is_empty() {
308 return xdg.join("harn").join("packs");
309 }
310 }
311 if let Some(home) = crate::user_dirs::home_dir() {
312 return home.join(".cache").join("harn").join("packs");
313 }
314 PathBuf::from(".harn-cache").join("packs")
315}
316
317pub fn cache_enabled() -> bool {
319 match std::env::var(CACHE_ENABLED_ENV).ok().as_deref() {
320 Some(value) => !matches!(
321 value.to_ascii_lowercase().as_str(),
322 "0" | "false" | "no" | "off"
323 ),
324 None => true,
325 }
326}
327
328pub fn load(source_path: &Path, source: &str) -> LookupOutcome {
332 let mut key = CacheKey {
336 source_hash: sha256(source.as_bytes()),
337 context_hash: [0u8; 32],
338 harn_version: Cow::Borrowed(HARN_VERSION),
339 compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
340 };
341 let mut walk = GraphWalk::new(source_path, source);
342
343 if !cache_enabled() {
344 let (context_hash, manifest) = walk.finish();
345 key.context_hash = context_hash;
346 return LookupOutcome {
347 key,
348 chunk: None,
349 manifest,
350 link_table: None,
351 };
352 }
353
354 let mut candidates: Vec<(PathBuf, bool)> = Vec::with_capacity(2);
355 if let Some(adjacent) = adjacent_cache_path(source_path) {
356 candidates.push((adjacent, true));
357 }
358 candidates.push((cache_dir().join(key.filename()), false));
359
360 let entry = module_source::canonical_identity(source_path);
365
366 for (path, allow_relocatable) in candidates {
367 let Ok(Some(candidate)) = read_entry_candidate(&path, &key) else {
368 continue;
369 };
370 match candidate
371 .manifest
372 .as_ref()
373 .map(|manifest| manifest.check(&entry))
374 {
375 Some(ManifestCheck::Valid) => {
378 key.context_hash = candidate.context_hash;
379 return LookupOutcome {
380 key,
381 chunk: Some(candidate.chunk),
382 link_table: candidate.manifest.as_ref().map(link_table_for),
383 manifest: candidate.manifest,
384 };
385 }
386 Some(ManifestCheck::ValidAfterRecheck { refreshed }) => {
391 key.context_hash = candidate.context_hash;
392 let _ = write_atomic_chunk(&path, &key, &candidate.chunk, Some(&refreshed));
393 return LookupOutcome {
394 key,
395 chunk: Some(candidate.chunk),
396 link_table: Some(link_table_for(&refreshed)),
397 manifest: Some(refreshed),
398 };
399 }
400 Some(ManifestCheck::Stale) | None => {}
401 }
402 if walk.context_hash() != candidate.context_hash {
403 if !allow_relocatable || walk.relocatable_context_hash() != candidate.context_hash {
404 continue;
405 }
406 key.context_hash = candidate.context_hash;
410 return LookupOutcome {
411 key,
412 chunk: Some(candidate.chunk),
413 manifest: walk.manifest().cloned(),
414 link_table: None,
415 };
416 }
417 key.context_hash = candidate.context_hash;
421 let manifest = walk.manifest().cloned();
422 let _ = write_atomic_chunk(&path, &key, &candidate.chunk, manifest.as_ref());
423 return LookupOutcome {
424 key,
425 chunk: Some(candidate.chunk),
426 manifest,
427 link_table: None,
428 };
429 }
430
431 let (context_hash, manifest) = walk.finish();
432 key.context_hash = context_hash;
433 LookupOutcome {
434 key,
435 chunk: None,
436 manifest,
437 link_table: None,
438 }
439}
440
441fn link_table_for(manifest: &ContextManifest) -> Arc<GraphLinkTable> {
445 Arc::new(GraphLinkTable::from_validated(manifest))
446}
447
448struct GraphWalk<'a> {
451 source_path: &'a Path,
452 source: &'a str,
453 result: Option<GraphHashes>,
454}
455
456impl<'a> GraphWalk<'a> {
457 fn new(source_path: &'a Path, source: &'a str) -> Self {
458 Self {
459 source_path,
460 source,
461 result: None,
462 }
463 }
464
465 fn run(&mut self) -> &GraphHashes {
466 self.result.get_or_insert_with(|| {
467 walk_import_graph_fingerprinted(self.source_path, self.source, CODEGEN_FINGERPRINT)
468 })
469 }
470
471 fn context_hash(&mut self) -> [u8; 32] {
472 self.run().canonical
473 }
474
475 fn relocatable_context_hash(&mut self) -> [u8; 32] {
476 self.run().relocatable
477 }
478
479 fn manifest(&mut self) -> Option<&ContextManifest> {
480 self.run().manifest.as_ref()
481 }
482
483 fn finish(mut self) -> ([u8; 32], Option<ContextManifest>) {
484 self.run();
485 let result = self.result.expect("the walk was just run");
486 (result.canonical, result.manifest)
487 }
488}
489
490pub fn store(key: &CacheKey, chunk: &Chunk, manifest: Option<&ContextManifest>) -> io::Result<()> {
494 if !cache_enabled() {
495 return Ok(());
496 }
497 let dir = cache_dir();
498 fs::create_dir_all(&dir)?;
499 write_atomic_chunk(&dir.join(key.filename()), key, chunk, manifest)
500}
501
502pub fn store_at(path: &Path, key: &CacheKey, chunk: &Chunk) -> io::Result<()> {
507 ensure_parent_dir(path)?;
508 write_atomic_chunk(path, key, chunk, None)
509}
510
511pub fn load_module(source_path: &Path, source: &ModuleSource) -> ModuleLookupOutcome {
514 load_module_for_key(source_path, CacheKey::from_module_source(source))
515}
516
517pub fn load_module_for_key(source_path: &Path, key: CacheKey) -> ModuleLookupOutcome {
524 if !cache_enabled() {
525 return ModuleLookupOutcome {
526 key,
527 artifact: None,
528 };
529 }
530 let mut candidates: Vec<PathBuf> = Vec::with_capacity(2);
531 if let Some(adjacent) = adjacent_module_cache_path(source_path) {
532 candidates.push(adjacent);
533 }
534 candidates.push(cache_dir().join(key.module_filename()));
535 for path in candidates {
536 match read_module_if_matches(&path, &key, source_path) {
537 Ok(Some(artifact)) => {
538 return ModuleLookupOutcome {
539 key,
540 artifact: Some(artifact),
541 }
542 }
543 Ok(None) => continue,
544 Err(_) => continue,
545 }
546 }
547 ModuleLookupOutcome {
548 key,
549 artifact: None,
550 }
551}
552
553pub fn store_module(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
556 if !cache_enabled() {
557 return Ok(());
558 }
559 let dir = cache_dir();
560 fs::create_dir_all(&dir)?;
561 write_atomic_module(&dir.join(key.module_filename()), key, artifact)
562}
563
564pub fn store_module_at(path: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
566 ensure_parent_dir(path)?;
567 write_atomic_module(path, key, artifact)
568}
569
570pub struct ModuleLookupOutcome {
573 pub key: CacheKey,
574 pub artifact: Option<ModuleArtifact>,
575}
576
577pub fn adjacent_cache_path(source_path: &Path) -> Option<PathBuf> {
580 adjacent_path_with_extension(source_path, CACHE_EXTENSION)
581}
582
583pub fn adjacent_module_cache_path(source_path: &Path) -> Option<PathBuf> {
586 adjacent_path_with_extension(source_path, MODULE_CACHE_EXTENSION)
587}
588
589fn adjacent_path_with_extension(source_path: &Path, ext: &str) -> Option<PathBuf> {
590 let stem = source_path.file_stem()?;
591 if stem.is_empty() {
592 return None;
593 }
594 let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
595 let mut out = parent.join(stem);
596 out.set_extension(ext);
597 Some(out)
598}
599
600fn ensure_parent_dir(path: &Path) -> io::Result<()> {
601 if let Some(parent) = path.parent() {
602 if !parent.as_os_str().is_empty() {
603 fs::create_dir_all(parent)?;
604 }
605 }
606 Ok(())
607}
608
609fn write_atomic_chunk(
610 target: &Path,
611 key: &CacheKey,
612 chunk: &Chunk,
613 manifest: Option<&ContextManifest>,
614) -> io::Result<()> {
615 let buf = serialize_chunk_artifact_with_manifest(key, chunk, manifest)?;
616 crate::atomic_io::atomic_write(target, &buf)
617}
618
619fn write_atomic_module(target: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
620 let buf = serialize_module_artifact(key, artifact)?;
621 crate::atomic_io::atomic_write(target, &buf)
622}
623
624pub fn serialize_chunk_artifact(key: &CacheKey, chunk: &Chunk) -> io::Result<Vec<u8>> {
630 serialize_chunk_artifact_with_manifest(key, chunk, None)
631}
632
633pub fn serialize_chunk_artifact_with_manifest(
641 key: &CacheKey,
642 chunk: &Chunk,
643 manifest: Option<&ContextManifest>,
644) -> io::Result<Vec<u8>> {
645 let payload = serialize_cache_payload(&EntryPayload {
646 manifest: manifest.cloned(),
647 chunk: chunk.freeze_for_cache(),
648 })?;
649 Ok(encode_artifact(key, KIND_ENTRY_CHUNK, &payload))
650}
651
652pub fn serialize_module_artifact(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<Vec<u8>> {
655 let payload = serialize_cache_payload(artifact)?;
656 Ok(encode_artifact(key, KIND_MODULE_ARTIFACT, &payload))
657}
658
659#[derive(serde::Serialize, serde::Deserialize)]
663struct EntryPayload {
664 manifest: Option<ContextManifest>,
665 chunk: CachedChunk,
666}
667
668fn serialize_cache_payload<T: Serialize>(value: &T) -> io::Result<Vec<u8>> {
669 postcard::to_allocvec(value)
670 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
671}
672
673fn deserialize_cache_payload<T: DeserializeOwned>(payload: &[u8]) -> Result<T, String> {
674 let (value, remaining) = postcard::take_from_bytes(payload).map_err(|err| err.to_string())?;
675 if remaining.is_empty() {
676 Ok(value)
677 } else {
678 Err("cache payload contains trailing bytes".to_string())
679 }
680}
681
682fn encode_artifact(key: &CacheKey, kind: u8, payload: &[u8]) -> Vec<u8> {
683 encode_artifact_fingerprinted(key, kind, payload, CODEGEN_FINGERPRINT)
684}
685
686fn encode_artifact_fingerprinted(
690 key: &CacheKey,
691 kind: u8,
692 payload: &[u8],
693 codegen_fingerprint: &str,
694) -> Vec<u8> {
695 let mut buf: Vec<u8> = Vec::with_capacity(payload.len() + 128);
696 buf.extend_from_slice(MAGIC);
697 buf.extend_from_slice(&SCHEMA_VERSION.to_le_bytes());
698 let version_bytes = key.harn_version.as_bytes();
699 buf.extend_from_slice(&(version_bytes.len() as u32).to_le_bytes());
700 buf.extend_from_slice(version_bytes);
701 let fingerprint_bytes = codegen_fingerprint.as_bytes();
702 buf.extend_from_slice(&(fingerprint_bytes.len() as u32).to_le_bytes());
703 buf.extend_from_slice(fingerprint_bytes);
704 buf.push(key.compiler_tag);
705 buf.push(kind);
706 buf.extend_from_slice(&key.source_hash);
707 buf.extend_from_slice(&key.context_hash);
708 buf.extend_from_slice(payload);
709 buf
710}
711
712fn read_length_prefixed_match(file: &mut fs::File, len: usize, expected: &[u8]) -> bool {
718 if len > 256 || len != expected.len() {
719 return false;
720 }
721 let mut buf = vec![0u8; len];
722 file.read_exact(&mut buf).is_ok() && buf == expected
723}
724
725struct ParsedHeader {
728 kind: u8,
729 context_hash: [u8; 32],
730 payload: Vec<u8>,
731}
732
733fn read_header_if_matches(
739 path: &Path,
740 key: &CacheKey,
741 expected_context: Option<&[u8; 32]>,
742) -> io::Result<Option<ParsedHeader>> {
743 let mut file = match fs::File::open(path) {
744 Ok(f) => f,
745 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
746 Err(err) => return Err(err),
747 };
748 let mut header = [0u8; 8 + 4 + 4];
749 if file.read_exact(&mut header).is_err() {
750 return Ok(None);
751 }
752 if &header[..8] != MAGIC {
753 return Ok(None);
754 }
755 let schema = u32::from_le_bytes(header[8..12].try_into().unwrap());
756 if schema != SCHEMA_VERSION {
757 return Ok(None);
758 }
759 let version_len = u32::from_le_bytes(header[12..16].try_into().unwrap()) as usize;
760 if !read_length_prefixed_match(&mut file, version_len, key.harn_version.as_bytes()) {
761 return Ok(None);
762 }
763 let mut fingerprint_len_bytes = [0u8; 4];
769 if file.read_exact(&mut fingerprint_len_bytes).is_err() {
770 return Ok(None);
771 }
772 let fingerprint_len = u32::from_le_bytes(fingerprint_len_bytes) as usize;
773 if !read_length_prefixed_match(&mut file, fingerprint_len, CODEGEN_FINGERPRINT.as_bytes()) {
774 return Ok(None);
775 }
776 let mut compiler_and_kind = [0u8; 2];
777 if file.read_exact(&mut compiler_and_kind).is_err() {
778 return Ok(None);
779 }
780 if compiler_and_kind[0] != key.compiler_tag {
781 return Ok(None);
782 }
783 let kind = compiler_and_kind[1];
784 let mut hashes = [0u8; 64];
785 if file.read_exact(&mut hashes).is_err() {
786 return Ok(None);
787 }
788 if hashes[..32] != key.source_hash {
789 return Ok(None);
790 }
791 let mut context_hash = [0u8; 32];
792 context_hash.copy_from_slice(&hashes[32..]);
793 if expected_context.is_some_and(|expected| *expected != context_hash) {
794 return Ok(None);
795 }
796 let mut payload = Vec::new();
797 if file.read_to_end(&mut payload).is_err() {
798 return Ok(None);
799 }
800 Ok(Some(ParsedHeader {
801 kind,
802 context_hash,
803 payload,
804 }))
805}
806
807struct CandidateEntry {
810 context_hash: [u8; 32],
811 manifest: Option<ContextManifest>,
812 chunk: Chunk,
813}
814
815fn read_entry_candidate(path: &Path, key: &CacheKey) -> io::Result<Option<CandidateEntry>> {
816 let Some(header) = read_header_if_matches(path, key, None)? else {
817 return Ok(None);
818 };
819 if header.kind != KIND_ENTRY_CHUNK {
820 return Ok(None);
821 }
822 let payload: EntryPayload = match deserialize_cache_payload(&header.payload) {
823 Ok(p) => p,
824 Err(_) => return Ok(None),
825 };
826 Ok(Some(CandidateEntry {
827 context_hash: header.context_hash,
828 manifest: payload.manifest,
829 chunk: Chunk::from_cached(payload.chunk),
830 }))
831}
832
833fn read_module_if_matches(
834 path: &Path,
835 key: &CacheKey,
836 source_path: &Path,
837) -> io::Result<Option<ModuleArtifact>> {
838 let Some(header) = read_header_if_matches(path, key, Some(&key.context_hash))? else {
839 return Ok(None);
840 };
841 if header.kind != KIND_MODULE_ARTIFACT {
842 return Ok(None);
843 }
844 match deserialize_cache_payload::<ModuleArtifact>(&header.payload) {
845 Ok(mut artifact) => {
846 artifact.bind_source_file(source_path);
847 Ok(Some(artifact))
848 }
849 Err(_) => Ok(None),
850 }
851}
852
853fn compiler_options_tag(options: CompilerOptions) -> u8 {
860 let mut tag: u8 = 0;
861 if options.optimizations_enabled() {
862 tag |= 0b0000_0001;
863 }
864 if options.legacy_ambient_capabilities() {
865 tag |= 0b0000_0010;
866 }
867 tag
868}
869
870fn sha256(bytes: &[u8]) -> [u8; 32] {
871 let mut hasher = Sha256::new();
872 hasher.update(bytes);
873 hasher.finalize().into()
874}
875
876fn hex(bytes: &[u8]) -> String {
877 let mut out = String::with_capacity(bytes.len() * 2);
878 for byte in bytes {
879 out.push_str(&format!("{byte:02x}"));
880 }
881 out
882}
883
884fn embedded_stdlib_digest() -> &'static [u8; 32] {
895 use std::sync::OnceLock;
896 static DIGEST: OnceLock<[u8; 32]> = OnceLock::new();
897 DIGEST.get_or_init(|| {
898 let mut entries: Vec<(&'static str, &'static str)> = harn_stdlib::STDLIB_SOURCES
899 .iter()
900 .map(|src| (src.module, src.source))
901 .collect();
902 entries.sort_by(|a, b| a.0.cmp(b.0));
903 let mut hasher = Sha256::new();
904 for (module, source) in entries {
905 hasher.update(module.as_bytes());
906 hasher.update(b"\0");
907 hasher.update(source.as_bytes());
908 hasher.update(b"\0");
909 }
910 hasher.finalize().into()
911 })
912}
913
914fn module_compilation_context_hash() -> [u8; 32] {
920 module_compilation_context_hash_fingerprinted(CODEGEN_FINGERPRINT)
921}
922
923fn module_compilation_context_hash_fingerprinted(codegen_fingerprint: &str) -> [u8; 32] {
924 let mut hasher = Sha256::new();
925 hasher.update(b"module-artifact-source-local-v3\0");
926 hasher.update(b"stdlib-digest\0");
927 hasher.update(embedded_stdlib_digest());
928 hasher.update(b"\0codegen-fingerprint\0");
929 hasher.update(codegen_fingerprint.as_bytes());
930 hasher.finalize().into()
931}
932
933#[cfg(test)]
941thread_local! {
942 pub(crate) static WALKS_PERFORMED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
943}
944
945fn hash_transitive_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
956 hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT).0
957}
958
959fn hash_relocatable_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
962 walk_import_graph_fingerprinted(source_path, source, CODEGEN_FINGERPRINT).relocatable
963}
964
965#[cfg(test)]
968fn hash_transitive_user_imports_with_manifest(
969 source_path: &Path,
970 source: &str,
971) -> ([u8; 32], Option<ContextManifest>) {
972 hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT)
973}
974
975fn hash_transitive_user_imports_fingerprinted(
979 source_path: &Path,
980 source: &str,
981 codegen_fingerprint: &str,
982) -> ([u8; 32], Option<ContextManifest>) {
983 let result = walk_import_graph_fingerprinted(source_path, source, codegen_fingerprint);
984 (result.canonical, result.manifest)
985}
986
987struct GraphHashes {
988 canonical: [u8; 32],
989 relocatable: [u8; 32],
990 manifest: Option<ContextManifest>,
991}
992
993fn walk_import_graph_fingerprinted(
994 source_path: &Path,
995 source: &str,
996 codegen_fingerprint: &str,
997) -> GraphHashes {
998 #[cfg(test)]
999 WALKS_PERFORMED.with(|c| c.set(c.get() + 1));
1000
1001 let mut visited: std::collections::BTreeMap<PathBuf, ImportNode> =
1002 std::collections::BTreeMap::new();
1003 let entry = ModuleSource::from_text(source);
1004 let mut frontier: Vec<(PathBuf, Arc<str>)> = entry
1005 .imports()
1006 .iter()
1007 .map(|import| (source_path.to_path_buf(), Arc::clone(import)))
1008 .collect();
1009 let mut manifest = Some(ContextManifest::begin(module_source::canonical_identity(
1016 source_path,
1017 )));
1018
1019 while let Some((anchor, import)) = frontier.pop() {
1020 let Some(resolved) = harn_modules::resolve_import_path(&anchor, &import) else {
1021 let sentinel = anchor.join(format!("__unresolved__/{import}"));
1025 if let std::collections::btree_map::Entry::Vacant(slot) = visited.entry(sentinel) {
1026 slot.insert(ImportNode::Unresolved {
1027 import: Arc::clone(&import),
1028 });
1029 if let Some(m) = manifest.as_mut() {
1030 m.unresolved.push(ManifestUnresolved {
1031 anchor: anchor.clone(),
1032 import: import.to_string(),
1033 });
1034 }
1035 }
1036 continue;
1037 };
1038 let canonical = module_source::canonical_identity(&resolved);
1039 if visited.contains_key(&canonical) {
1040 continue;
1041 }
1042 match module_source::read(&resolved) {
1049 Ok(module) => {
1050 visited.insert(
1051 canonical.clone(),
1052 ImportNode::Resolved {
1053 content: Arc::clone(module.text()),
1054 },
1055 );
1056 match ManifestFile::observe(&canonical, &module) {
1057 Some(file) => {
1058 if let Some(m) = manifest.as_mut() {
1059 m.files.push(file);
1060 }
1061 }
1062 None => manifest = None,
1066 }
1067 for nested_import in module.imports() {
1068 frontier.push((resolved.clone(), Arc::clone(nested_import)));
1069 }
1070 }
1071 Err(error) => {
1072 let unreadable_path = canonical.clone();
1073 visited.insert(
1074 canonical,
1075 ImportNode::IoError {
1076 kind: error.kind().to_string(),
1077 },
1078 );
1079 if let Some(m) = manifest.as_mut() {
1084 m.unreadable.push(ManifestUnreadable {
1085 path: unreadable_path,
1086 kind: error.kind().to_string(),
1087 });
1088 }
1089 }
1090 }
1091 }
1092
1093 let mut canonical_hasher = Sha256::new();
1094 seed_entry_context_hasher(&mut canonical_hasher, codegen_fingerprint);
1095 let mut relocatable_hasher = Sha256::new();
1096 relocatable_hasher.update(b"relocatable-entry-graph-v1\0");
1097 seed_entry_context_hasher(&mut relocatable_hasher, codegen_fingerprint);
1098
1099 let entry_identity = module_source::canonical_identity(source_path);
1100 let entry_dir = entry_identity.parent().unwrap_or(Path::new(""));
1101 let mut relocatable_nodes = Vec::with_capacity(visited.len());
1102 for (path, node) in &visited {
1103 canonical_hasher.update(path.to_string_lossy().as_bytes());
1104 canonical_hasher.update(b"\0");
1105 hash_import_node(&mut canonical_hasher, node);
1106 canonical_hasher.update(b"\0");
1107
1108 let Some(label) = relative_path_label(entry_dir, path) else {
1109 relocatable_nodes.push((path.to_string_lossy().replace('\\', "/"), node));
1113 continue;
1114 };
1115 relocatable_nodes.push((label, node));
1116 }
1117 relocatable_nodes.sort_by(|left, right| left.0.cmp(&right.0));
1118 for (path, node) in relocatable_nodes {
1119 relocatable_hasher.update(path.as_bytes());
1120 relocatable_hasher.update(b"\0");
1121 hash_import_node(&mut relocatable_hasher, node);
1122 relocatable_hasher.update(b"\0");
1123 }
1124
1125 if let Some(m) = manifest.as_mut() {
1128 m.files.sort_by(|a, b| a.path.cmp(&b.path));
1129 m.unresolved
1130 .sort_by(|a, b| (&a.anchor, &a.import).cmp(&(&b.anchor, &b.import)));
1131 m.unreadable.sort_by(|a, b| a.path.cmp(&b.path));
1132 }
1133 GraphHashes {
1134 canonical: canonical_hasher.finalize().into(),
1135 relocatable: relocatable_hasher.finalize().into(),
1136 manifest,
1137 }
1138}
1139
1140fn seed_entry_context_hasher(hasher: &mut Sha256, codegen_fingerprint: &str) {
1141 hasher.update(b"stdlib-digest\0");
1142 hasher.update(embedded_stdlib_digest());
1143 hasher.update(b"\0");
1144 hasher.update(b"codegen-fingerprint\0");
1149 hasher.update(codegen_fingerprint.as_bytes());
1150 hasher.update(b"\0");
1151}
1152
1153fn hash_import_node(hasher: &mut Sha256, node: &ImportNode) {
1154 match node {
1155 ImportNode::Resolved { content } => {
1156 hasher.update(b"resolved\0");
1157 hasher.update(content.as_bytes());
1158 }
1159 ImportNode::Unresolved { import } => {
1160 hasher.update(b"unresolved\0");
1161 hasher.update(import.as_bytes());
1162 }
1163 ImportNode::IoError { kind } => {
1164 hasher.update(b"ioerror\0");
1165 hasher.update(kind.as_bytes());
1166 }
1167 }
1168}
1169
1170fn relative_path_label(base: &Path, target: &Path) -> Option<String> {
1174 let base_components = base.components().collect::<Vec<_>>();
1175 let target_components = target.components().collect::<Vec<_>>();
1176 let common = base_components
1177 .iter()
1178 .zip(&target_components)
1179 .take_while(|(left, right)| left == right)
1180 .count();
1181 if common == 0 && (base.is_absolute() || target.is_absolute()) {
1182 return None;
1183 }
1184
1185 let mut parts = Vec::new();
1186 for component in &base_components[common..] {
1187 if matches!(component, std::path::Component::Normal(_)) {
1188 parts.push("..".to_string());
1189 }
1190 }
1191 for component in &target_components[common..] {
1192 match component {
1193 std::path::Component::Normal(part) => parts.push(part.to_string_lossy().into_owned()),
1194 std::path::Component::ParentDir => parts.push("..".to_string()),
1195 std::path::Component::CurDir => {}
1196 std::path::Component::RootDir | std::path::Component::Prefix(_) => return None,
1197 }
1198 }
1199 Some(if parts.is_empty() {
1200 ".".to_string()
1201 } else {
1202 parts.join("/")
1203 })
1204}
1205
1206enum ImportNode {
1207 Resolved { content: Arc<str> },
1208 Unresolved { import: Arc<str> },
1209 IoError { kind: String },
1210}
1211
1212#[cfg(test)]
1213#[path = "bytecode_cache_tests.rs"]
1214mod tests;