use std::borrow::Cow;
use std::fs;
use std::io::{self, Read as _};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use serde::{de::DeserializeOwned, Serialize};
use sha2::{Digest, Sha256};
use crate::chunk::{CachedChunk, Chunk};
use crate::compiler::CompilerOptions;
use crate::context_manifest::{
ContextManifest, GraphLinkTable, ManifestCheck, ManifestFile, ManifestUnreadable,
ManifestUnresolved,
};
use crate::module_artifact::{ModuleArtifact, ModuleCompilationContext, ModuleProvenance};
use crate::module_source::{self, ModuleSource};
mod graph;
pub(crate) use graph::derive_interface as module_compilation_context_with_manifest;
pub use graph::prepare_entry_store;
use graph::relative_path_label;
pub const MAGIC: &[u8; 8] = b"HARNBC\0\0";
pub const SCHEMA_VERSION: u32 = 14;
pub const HARN_VERSION: &str = env!("CARGO_PKG_VERSION");
pub const CODEGEN_FINGERPRINT: &str = env!("HARN_CODEGEN_FINGERPRINT");
pub const CACHE_EXTENSION: &str = "harnbc";
pub const MODULE_CACHE_EXTENSION: &str = "harnmod";
const KIND_ENTRY_CHUNK: u8 = 1;
const KIND_MODULE_ARTIFACT: u8 = 2;
pub const CACHE_DIR_ENV: &str = "HARN_CACHE_DIR";
pub const CACHE_ENABLED_ENV: &str = "HARN_BYTECODE_CACHE";
pub struct LookupOutcome {
pub key: CacheKey,
pub chunk: Option<Chunk>,
pub manifest: Option<ContextManifest>,
pub link_table: Option<Arc<GraphLinkTable>>,
}
impl LookupOutcome {
pub fn store(&self, chunk: &Chunk) -> io::Result<()> {
store(&self.key, chunk, self.manifest.as_ref())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CacheKey {
pub source_hash: [u8; 32],
pub context_hash: [u8; 32],
pub harn_version: Cow<'static, str>,
pub compiler_tag: u8,
pub provenance: ModuleProvenance,
}
impl CacheKey {
pub fn from_source(source_path: &Path, source: &str) -> Self {
let source_hash = sha256(source.as_bytes());
let context_hash = hash_transitive_user_imports(source_path, source);
Self {
source_hash,
context_hash,
harn_version: Cow::Borrowed(HARN_VERSION),
compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
provenance: ModuleProvenance::User,
}
}
pub fn from_relocatable_source(source_path: &Path, source: &str) -> Self {
let source_hash = sha256(source.as_bytes());
let context_hash = hash_relocatable_user_imports(source_path, source);
Self {
source_hash,
context_hash,
harn_version: Cow::Borrowed(HARN_VERSION),
compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
provenance: ModuleProvenance::User,
}
}
#[must_use]
pub fn for_artifact_version(mut self, harn_version: impl Into<String>) -> Self {
self.harn_version = Cow::Owned(harn_version.into());
self
}
pub fn from_module_source(
source: &ModuleSource,
compilation_context: &ModuleCompilationContext,
provenance: ModuleProvenance,
) -> Self {
Self::from_module_content_hash(source.sha256(), compilation_context, provenance)
}
pub fn from_module_content_hash(
content_hash: [u8; 32],
compilation_context: &ModuleCompilationContext,
provenance: ModuleProvenance,
) -> Self {
Self {
source_hash: content_hash,
context_hash: module_compilation_context_hash(compilation_context),
harn_version: Cow::Borrowed(HARN_VERSION),
compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
provenance,
}
}
pub fn from_embedded_stdlib_module_content_hash(
content_hash: [u8; 32],
provenance: ModuleProvenance,
) -> Self {
Self {
source_hash: content_hash,
context_hash: module_compilation_context_hash_fingerprinted(
CODEGEN_FINGERPRINT,
EMBEDDED_STDLIB_INTERFACE_DIGEST,
),
harn_version: Cow::Borrowed(HARN_VERSION),
compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
provenance,
}
}
pub fn filename(&self) -> String {
format!("{}.{}", hex(&self.source_hash), CACHE_EXTENSION)
}
pub fn module_filename(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.source_hash);
hasher.update(self.context_hash);
hasher.update(self.harn_version.as_bytes());
hasher.update([self.compiler_tag]);
hasher.update([provenance_tag(self.provenance)]);
let identity: [u8; 32] = hasher.finalize().into();
format!("{}.{}", hex(&identity), MODULE_CACHE_EXTENSION)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheDirError {
Empty,
Relative(PathBuf),
}
impl std::fmt::Display for CacheDirError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => write!(
formatter,
"{CACHE_DIR_ENV} is set but empty; unset it to use the default cache location, \
or set it to an absolute path"
),
Self::Relative(path) => write!(
formatter,
"{CACHE_DIR_ENV} must be an absolute path, got {}; a relative cache directory \
moves with the working directory, so the same process run from two directories \
would keep two unrelated caches",
path.display()
),
}
}
}
impl std::error::Error for CacheDirError {}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CacheRoot {
Explicit(PathBuf),
Discovered(PathBuf),
}
impl CacheRoot {
fn bytecode_dir(&self) -> PathBuf {
match self {
Self::Explicit(path) => path.clone(),
Self::Discovered(path) => path.join("bytecode"),
}
}
fn packs_dir(&self) -> PathBuf {
match self {
Self::Explicit(path) | Self::Discovered(path) => path.join("packs"),
}
}
}
fn cache_root() -> Result<Option<CacheRoot>, CacheDirError> {
cache_root_from(
std::env::var_os(CACHE_DIR_ENV).map(PathBuf::from),
std::env::var_os("XDG_CACHE_HOME").map(PathBuf::from),
crate::user_dirs::home_dir(),
)
}
fn cache_root_from(
explicit: Option<PathBuf>,
xdg: Option<PathBuf>,
home: Option<PathBuf>,
) -> Result<Option<CacheRoot>, CacheDirError> {
if let Some(custom) = explicit {
if custom.as_os_str().is_empty() {
return Err(CacheDirError::Empty);
}
if !custom.is_absolute() {
return Err(CacheDirError::Relative(custom));
}
return Ok(Some(CacheRoot::Explicit(custom)));
}
if let Some(xdg) = xdg {
if !xdg.as_os_str().is_empty() && xdg.is_absolute() {
return Ok(Some(CacheRoot::Discovered(xdg.join("harn"))));
}
}
if let Some(home) = home {
return Ok(Some(CacheRoot::Discovered(
home.join(".cache").join("harn"),
)));
}
Ok(None)
}
pub fn check_cache_config() -> Result<Option<&'static str>, CacheDirError> {
Ok(cache_root()?.is_none().then_some(
"no cache directory resolves (no HARN_CACHE_DIR, no XDG_CACHE_HOME, no home \
directory); compiled bytecode will not be cached for this run",
))
}
pub fn cache_dir() -> Option<PathBuf> {
cache_root().ok().flatten().map(|root| root.bytecode_dir())
}
pub fn packs_cache_dir() -> Option<PathBuf> {
cache_root().ok().flatten().map(|root| root.packs_dir())
}
pub fn cache_enabled() -> bool {
let switched_on = match std::env::var(CACHE_ENABLED_ENV).ok().as_deref() {
Some(value) => !matches!(
value.to_ascii_lowercase().as_str(),
"0" | "false" | "no" | "off"
),
None => true,
};
switched_on && matches!(cache_root(), Ok(Some(_)))
}
pub fn load(source_path: &Path, source: &str) -> LookupOutcome {
let mut key = CacheKey {
source_hash: sha256(source.as_bytes()),
context_hash: [0u8; 32],
harn_version: Cow::Borrowed(HARN_VERSION),
compiler_tag: compiler_options_tag(CompilerOptions::from_env()),
provenance: ModuleProvenance::User,
};
let mut walk = GraphWalk::new(source_path, source);
if !cache_enabled() {
let (context_hash, manifest) = walk.finish();
key.context_hash = context_hash;
return LookupOutcome {
key,
chunk: None,
manifest,
link_table: None,
};
}
let mut candidates: Vec<(PathBuf, bool)> = Vec::with_capacity(2);
if let Some(adjacent) = adjacent_cache_path(source_path) {
candidates.push((adjacent, true));
}
if let Some(dir) = cache_dir() {
candidates.push((dir.join(key.filename()), false));
}
let entry = module_source::canonical_identity(source_path);
for (path, allow_relocatable) in candidates {
let Ok(Some(candidate)) = read_entry_candidate(&path, &key) else {
continue;
};
match candidate
.manifest
.as_ref()
.map(|manifest| manifest.check(&entry))
{
Some(ManifestCheck::Valid) => {
key.context_hash = candidate.context_hash;
return LookupOutcome {
key,
chunk: Some(candidate.chunk),
link_table: candidate.manifest.as_ref().map(link_table_for),
manifest: candidate.manifest,
};
}
Some(ManifestCheck::ValidAfterRecheck { refreshed }) => {
key.context_hash = candidate.context_hash;
let _ = write_atomic_chunk(&path, &key, &candidate.chunk, Some(&refreshed));
return LookupOutcome {
key,
chunk: Some(candidate.chunk),
link_table: Some(link_table_for(&refreshed)),
manifest: Some(refreshed),
};
}
Some(ManifestCheck::Stale) | None => {}
}
if walk.context_hash() != candidate.context_hash {
if !allow_relocatable || walk.relocatable_context_hash() != candidate.context_hash {
continue;
}
key.context_hash = candidate.context_hash;
return LookupOutcome {
key,
chunk: Some(candidate.chunk),
manifest: walk.manifest().cloned(),
link_table: None,
};
}
key.context_hash = candidate.context_hash;
let manifest = walk.manifest().cloned();
let _ = write_atomic_chunk(&path, &key, &candidate.chunk, manifest.as_ref());
return LookupOutcome {
key,
chunk: Some(candidate.chunk),
manifest,
link_table: None,
};
}
let (context_hash, manifest) = walk.finish();
key.context_hash = context_hash;
LookupOutcome {
key,
chunk: None,
manifest,
link_table: None,
}
}
fn link_table_for(manifest: &ContextManifest) -> Arc<GraphLinkTable> {
Arc::new(GraphLinkTable::from_validated(manifest))
}
struct GraphWalk<'a> {
source_path: &'a Path,
source: &'a str,
result: Option<GraphHashes>,
}
impl<'a> GraphWalk<'a> {
fn new(source_path: &'a Path, source: &'a str) -> Self {
Self {
source_path,
source,
result: None,
}
}
fn run(&mut self) -> &GraphHashes {
self.result.get_or_insert_with(|| {
walk_import_graph_fingerprinted(
self.source_path,
self.source,
CODEGEN_FINGERPRINT,
false,
)
})
}
fn context_hash(&mut self) -> [u8; 32] {
self.run().canonical
}
fn relocatable_context_hash(&mut self) -> [u8; 32] {
self.run().relocatable
}
fn manifest(&mut self) -> Option<&ContextManifest> {
self.run().manifest.as_ref()
}
fn finish(mut self) -> ([u8; 32], Option<ContextManifest>) {
self.run();
let result = self.result.expect("the walk was just run");
(result.canonical, result.manifest)
}
}
pub fn store(key: &CacheKey, chunk: &Chunk, manifest: Option<&ContextManifest>) -> io::Result<()> {
if !cache_enabled() {
return Ok(());
}
let Some(dir) = cache_dir() else {
return Ok(());
};
fs::create_dir_all(&dir)?;
write_atomic_chunk(&dir.join(key.filename()), key, chunk, manifest)
}
pub fn store_at(path: &Path, key: &CacheKey, chunk: &Chunk) -> io::Result<()> {
ensure_parent_dir(path)?;
write_atomic_chunk(path, key, chunk, None)
}
pub fn load_module(
source_path: &Path,
source: &ModuleSource,
compilation_context: &ModuleCompilationContext,
provenance: ModuleProvenance,
) -> ModuleLookupOutcome {
load_module_for_key(
source_path,
CacheKey::from_module_source(source, compilation_context, provenance),
)
}
pub fn load_module_for_key(source_path: &Path, key: CacheKey) -> ModuleLookupOutcome {
if !cache_enabled() {
return ModuleLookupOutcome {
key,
artifact: None,
};
}
let mut candidates: Vec<PathBuf> = Vec::with_capacity(2);
if let Some(adjacent) = adjacent_module_cache_path(source_path) {
candidates.push(adjacent);
}
if let Some(dir) = cache_dir() {
candidates.push(dir.join(key.module_filename()));
}
for path in candidates {
match read_module_if_matches(&path, &key, source_path) {
Ok(Some(artifact)) => {
return ModuleLookupOutcome {
key,
artifact: Some(artifact),
}
}
Ok(None) => continue,
Err(_) => continue,
}
}
ModuleLookupOutcome {
key,
artifact: None,
}
}
pub fn store_module(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
if !cache_enabled() {
return Ok(());
}
let Some(dir) = cache_dir() else {
return Ok(());
};
fs::create_dir_all(&dir)?;
write_atomic_module(&dir.join(key.module_filename()), key, artifact)
}
pub fn store_module_at(path: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
ensure_parent_dir(path)?;
write_atomic_module(path, key, artifact)
}
pub struct ModuleLookupOutcome {
pub key: CacheKey,
pub artifact: Option<ModuleArtifact>,
}
pub fn adjacent_cache_path(source_path: &Path) -> Option<PathBuf> {
adjacent_path_with_extension(source_path, CACHE_EXTENSION)
}
pub fn adjacent_module_cache_path(source_path: &Path) -> Option<PathBuf> {
adjacent_path_with_extension(source_path, MODULE_CACHE_EXTENSION)
}
fn adjacent_path_with_extension(source_path: &Path, ext: &str) -> Option<PathBuf> {
let stem = source_path.file_stem()?;
if stem.is_empty() {
return None;
}
let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
let mut out = parent.join(stem);
out.set_extension(ext);
Some(out)
}
fn ensure_parent_dir(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
Ok(())
}
const ARTIFACT_MODE: u32 = 0o600;
fn write_atomic_chunk(
target: &Path,
key: &CacheKey,
chunk: &Chunk,
manifest: Option<&ContextManifest>,
) -> io::Result<()> {
let buf = serialize_chunk_artifact_with_manifest(key, chunk, manifest)?;
crate::atomic_io::atomic_write_with_mode(target, &buf, ARTIFACT_MODE)
}
fn write_atomic_module(target: &Path, key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<()> {
let buf = serialize_module_artifact(key, artifact)?;
crate::atomic_io::atomic_write_with_mode(target, &buf, ARTIFACT_MODE)
}
pub fn serialize_chunk_artifact(key: &CacheKey, chunk: &Chunk) -> io::Result<Vec<u8>> {
serialize_chunk_artifact_with_manifest(key, chunk, None)
}
pub fn serialize_chunk_artifact_with_manifest(
key: &CacheKey,
chunk: &Chunk,
manifest: Option<&ContextManifest>,
) -> io::Result<Vec<u8>> {
let payload = serialize_cache_payload(&EntryPayload {
manifest: manifest.cloned(),
chunk: chunk.freeze_for_cache(),
})?;
Ok(encode_artifact(key, KIND_ENTRY_CHUNK, &payload))
}
pub fn serialize_module_artifact(key: &CacheKey, artifact: &ModuleArtifact) -> io::Result<Vec<u8>> {
let payload = serialize_cache_payload(artifact)?;
Ok(encode_artifact(key, KIND_MODULE_ARTIFACT, &payload))
}
#[derive(serde::Serialize, serde::Deserialize)]
struct EntryPayload {
manifest: Option<ContextManifest>,
chunk: CachedChunk,
}
fn serialize_cache_payload<T: Serialize>(value: &T) -> io::Result<Vec<u8>> {
postcard::to_allocvec(value)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
}
fn deserialize_cache_payload<T: DeserializeOwned>(payload: &[u8]) -> Result<T, String> {
let (value, remaining) = postcard::take_from_bytes(payload).map_err(|err| err.to_string())?;
if remaining.is_empty() {
Ok(value)
} else {
Err("cache payload contains trailing bytes".to_string())
}
}
fn encode_artifact(key: &CacheKey, kind: u8, payload: &[u8]) -> Vec<u8> {
encode_artifact_fingerprinted(key, kind, payload, CODEGEN_FINGERPRINT)
}
fn encode_artifact_fingerprinted(
key: &CacheKey,
kind: u8,
payload: &[u8],
codegen_fingerprint: &str,
) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::with_capacity(payload.len() + 128);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&SCHEMA_VERSION.to_le_bytes());
let version_bytes = key.harn_version.as_bytes();
buf.extend_from_slice(&(version_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(version_bytes);
let fingerprint_bytes = codegen_fingerprint.as_bytes();
buf.extend_from_slice(&(fingerprint_bytes.len() as u32).to_le_bytes());
buf.extend_from_slice(fingerprint_bytes);
buf.push(key.compiler_tag);
buf.push(kind);
buf.push(provenance_tag(key.provenance));
buf.extend_from_slice(&key.source_hash);
buf.extend_from_slice(&key.context_hash);
buf.extend_from_slice(payload);
buf
}
fn provenance_tag(provenance: ModuleProvenance) -> u8 {
match provenance {
ModuleProvenance::User => 0,
ModuleProvenance::EmbeddedStdlib => 1,
ModuleProvenance::PrivilegedWire => 2,
ModuleProvenance::TrustedHostDispatch => 3,
}
}
fn read_length_prefixed_match(file: &mut fs::File, len: usize, expected: &[u8]) -> bool {
if len > 256 || len != expected.len() {
return false;
}
let mut buf = vec![0u8; len];
file.read_exact(&mut buf).is_ok() && buf == expected
}
struct ParsedHeader {
kind: u8,
context_hash: [u8; 32],
payload: Vec<u8>,
}
fn read_header_if_matches(
path: &Path,
key: &CacheKey,
expected_context: Option<&[u8; 32]>,
) -> io::Result<Option<ParsedHeader>> {
let mut file = match fs::File::open(path) {
Ok(f) => f,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(err) => return Err(err),
};
let mut header = [0u8; 8 + 4 + 4];
if file.read_exact(&mut header).is_err() {
return Ok(None);
}
if &header[..8] != MAGIC {
return Ok(None);
}
let schema = u32::from_le_bytes(header[8..12].try_into().unwrap());
if schema != SCHEMA_VERSION {
return Ok(None);
}
let version_len = u32::from_le_bytes(header[12..16].try_into().unwrap()) as usize;
if !read_length_prefixed_match(&mut file, version_len, key.harn_version.as_bytes()) {
return Ok(None);
}
let mut fingerprint_len_bytes = [0u8; 4];
if file.read_exact(&mut fingerprint_len_bytes).is_err() {
return Ok(None);
}
let fingerprint_len = u32::from_le_bytes(fingerprint_len_bytes) as usize;
if !read_length_prefixed_match(&mut file, fingerprint_len, CODEGEN_FINGERPRINT.as_bytes()) {
return Ok(None);
}
let mut compiler_kind_provenance = [0u8; 3];
if file.read_exact(&mut compiler_kind_provenance).is_err() {
return Ok(None);
}
if compiler_kind_provenance[0] != key.compiler_tag {
return Ok(None);
}
let kind = compiler_kind_provenance[1];
if compiler_kind_provenance[2] != provenance_tag(key.provenance) {
return Ok(None);
}
let mut hashes = [0u8; 64];
if file.read_exact(&mut hashes).is_err() {
return Ok(None);
}
if hashes[..32] != key.source_hash {
return Ok(None);
}
let mut context_hash = [0u8; 32];
context_hash.copy_from_slice(&hashes[32..]);
if expected_context.is_some_and(|expected| *expected != context_hash) {
return Ok(None);
}
let mut payload = Vec::new();
if file.read_to_end(&mut payload).is_err() {
return Ok(None);
}
Ok(Some(ParsedHeader {
kind,
context_hash,
payload,
}))
}
struct CandidateEntry {
context_hash: [u8; 32],
manifest: Option<ContextManifest>,
chunk: Chunk,
}
fn read_entry_candidate(path: &Path, key: &CacheKey) -> io::Result<Option<CandidateEntry>> {
let Some(header) = read_header_if_matches(path, key, None)? else {
return Ok(None);
};
if header.kind != KIND_ENTRY_CHUNK {
return Ok(None);
}
let payload: EntryPayload = match deserialize_cache_payload(&header.payload) {
Ok(p) => p,
Err(_) => return Ok(None),
};
Ok(Some(CandidateEntry {
context_hash: header.context_hash,
manifest: payload.manifest,
chunk: Chunk::from_cached(payload.chunk),
}))
}
fn read_module_if_matches(
path: &Path,
key: &CacheKey,
source_path: &Path,
) -> io::Result<Option<ModuleArtifact>> {
let Some(header) = read_header_if_matches(path, key, Some(&key.context_hash))? else {
return Ok(None);
};
if header.kind != KIND_MODULE_ARTIFACT {
return Ok(None);
}
match deserialize_cache_payload::<ModuleArtifact>(&header.payload) {
Ok(mut artifact) => {
artifact.bind_source_file(source_path);
Ok(Some(artifact))
}
Err(_) => Ok(None),
}
}
fn compiler_options_tag(options: CompilerOptions) -> u8 {
let mut tag: u8 = 0;
if options.optimizations_enabled() {
tag |= 0b0000_0001;
}
if options.legacy_ambient_capabilities() {
tag |= 0b0000_0010;
}
tag
}
fn sha256(bytes: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(bytes);
hasher.finalize().into()
}
fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push_str(&format!("{byte:02x}"));
}
out
}
fn embedded_stdlib_digest() -> &'static [u8; 32] {
use std::sync::OnceLock;
static DIGEST: OnceLock<[u8; 32]> = OnceLock::new();
DIGEST.get_or_init(|| {
let mut entries: Vec<(&'static str, &'static str)> = harn_stdlib::STDLIB_SOURCES
.iter()
.map(|src| (src.module, src.source))
.collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
let mut hasher = Sha256::new();
for (module, source) in entries {
hasher.update(module.as_bytes());
hasher.update(b"\0");
hasher.update(source.as_bytes());
hasher.update(b"\0");
}
hasher.finalize().into()
})
}
fn module_compilation_context_hash(compilation_context: &ModuleCompilationContext) -> [u8; 32] {
module_compilation_context_hash_fingerprinted(CODEGEN_FINGERPRINT, compilation_context.digest())
}
const EMBEDDED_STDLIB_INTERFACE_DIGEST: [u8; 32] = *b"harn.embedded-stdlib.interface\0\0";
fn module_compilation_context_hash_fingerprinted(
codegen_fingerprint: &str,
imported_interface_digest: [u8; 32],
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"module-artifact-source-local-v4\0");
hasher.update(b"stdlib-digest\0");
hasher.update(embedded_stdlib_digest());
hasher.update(b"\0codegen-fingerprint\0");
hasher.update(codegen_fingerprint.as_bytes());
hasher.update(b"\0imported-interface\0");
hasher.update(imported_interface_digest);
hasher.finalize().into()
}
#[cfg(test)]
thread_local! {
pub(crate) static WALKS_PERFORMED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
fn hash_transitive_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT).0
}
fn hash_relocatable_user_imports(source_path: &Path, source: &str) -> [u8; 32] {
walk_import_graph_fingerprinted(source_path, source, CODEGEN_FINGERPRINT, false).relocatable
}
#[cfg(test)]
fn hash_transitive_user_imports_with_manifest(
source_path: &Path,
source: &str,
) -> ([u8; 32], Option<ContextManifest>) {
hash_transitive_user_imports_fingerprinted(source_path, source, CODEGEN_FINGERPRINT)
}
fn hash_transitive_user_imports_fingerprinted(
source_path: &Path,
source: &str,
codegen_fingerprint: &str,
) -> ([u8; 32], Option<ContextManifest>) {
let result = walk_import_graph_fingerprinted(source_path, source, codegen_fingerprint, false);
(result.canonical, result.manifest)
}
struct GraphHashes {
canonical: [u8; 32],
relocatable: [u8; 32],
manifest: Option<ContextManifest>,
entry_compilation_context: Option<ModuleCompilationContext>,
}
fn walk_import_graph_fingerprinted(
source_path: &Path,
source: &str,
codegen_fingerprint: &str,
capture_entry_compilation_context: bool,
) -> GraphHashes {
#[cfg(test)]
WALKS_PERFORMED.with(|c| c.set(c.get() + 1));
let mut visited: std::collections::BTreeMap<PathBuf, ImportNode> =
std::collections::BTreeMap::new();
let entry = ModuleSource::from_text(source);
let mut frontier: Vec<(PathBuf, Arc<str>)> = entry
.imports()
.iter()
.map(|import| (source_path.to_path_buf(), Arc::clone(import)))
.collect();
let mut manifest = Some(ContextManifest::begin(module_source::canonical_identity(
source_path,
)));
while let Some((anchor, import)) = frontier.pop() {
let Some(resolved) = harn_modules::resolve_import_path(&anchor, &import) else {
let sentinel = anchor.join(format!("__unresolved__/{import}"));
if let std::collections::btree_map::Entry::Vacant(slot) = visited.entry(sentinel) {
slot.insert(ImportNode::Unresolved {
import: Arc::clone(&import),
});
if let Some(m) = manifest.as_mut() {
m.unresolved.push(ManifestUnresolved {
anchor: anchor.clone(),
import: import.to_string(),
});
}
}
continue;
};
let canonical = module_source::canonical_identity(&resolved);
if visited.contains_key(&canonical) {
continue;
}
match module_source::read(&resolved) {
Ok(module) => {
visited.insert(
canonical.clone(),
ImportNode::Resolved {
content: Arc::clone(module.text()),
},
);
match ManifestFile::observe(&canonical, &module) {
Some(file) => {
if let Some(m) = manifest.as_mut() {
m.files.push(file);
}
}
None => manifest = None,
}
for nested_import in module.imports() {
frontier.push((resolved.clone(), Arc::clone(nested_import)));
}
}
Err(error) => {
let unreadable_path = canonical.clone();
visited.insert(
canonical,
ImportNode::IoError {
kind: error.kind().to_string(),
},
);
if let Some(m) = manifest.as_mut() {
m.unreadable.push(ManifestUnreadable {
path: unreadable_path,
kind: error.kind().to_string(),
});
}
}
}
}
let mut entry_compilation_context = None;
if manifest.is_some() {
let graph = harn_modules::build_with_source(source_path, source);
manifest
.as_mut()
.expect("manifest presence checked")
.package_import_aliases = graph.package_import_aliases();
if capture_entry_compilation_context {
entry_compilation_context =
ModuleCompilationContext::for_source_in_graph(&graph, source_path, source).ok();
}
let contexts = manifest
.as_ref()
.expect("manifest presence checked")
.files
.iter()
.map(|file| match visited.get(&file.path) {
Some(ImportNode::Resolved { content }) => {
ModuleCompilationContext::for_source_in_graph(
&graph,
&file.path,
content.as_ref(),
)
.ok()
}
_ => None,
})
.collect::<Option<Vec<_>>>();
if let Some(contexts) = contexts {
for (file, context) in manifest
.as_mut()
.expect("the manifest was just borrowed")
.files
.iter_mut()
.zip(contexts)
{
file.compilation_context = context;
}
} else {
manifest = None;
entry_compilation_context = None;
}
}
let mut canonical_hasher = Sha256::new();
seed_entry_context_hasher(&mut canonical_hasher, codegen_fingerprint);
let mut relocatable_hasher = Sha256::new();
relocatable_hasher.update(b"relocatable-entry-graph-v1\0");
seed_entry_context_hasher(&mut relocatable_hasher, codegen_fingerprint);
let entry_identity = module_source::canonical_identity(source_path);
let entry_dir = entry_identity.parent().unwrap_or(Path::new(""));
let mut relocatable_nodes = Vec::with_capacity(visited.len());
for (path, node) in &visited {
canonical_hasher.update(path.to_string_lossy().as_bytes());
canonical_hasher.update(b"\0");
hash_import_node(&mut canonical_hasher, node);
canonical_hasher.update(b"\0");
let Some(label) = relative_path_label(entry_dir, path) else {
relocatable_nodes.push((path.to_string_lossy().replace('\\', "/"), node));
continue;
};
relocatable_nodes.push((label, node));
}
relocatable_nodes.sort_by(|left, right| left.0.cmp(&right.0));
for (path, node) in relocatable_nodes {
relocatable_hasher.update(path.as_bytes());
relocatable_hasher.update(b"\0");
hash_import_node(&mut relocatable_hasher, node);
relocatable_hasher.update(b"\0");
}
if let Some(m) = manifest.as_mut() {
m.files.sort_by(|a, b| a.path.cmp(&b.path));
m.unresolved
.sort_by(|a, b| (&a.anchor, &a.import).cmp(&(&b.anchor, &b.import)));
m.unreadable.sort_by(|a, b| a.path.cmp(&b.path));
}
GraphHashes {
canonical: canonical_hasher.finalize().into(),
relocatable: relocatable_hasher.finalize().into(),
manifest,
entry_compilation_context,
}
}
fn seed_entry_context_hasher(hasher: &mut Sha256, codegen_fingerprint: &str) {
hasher.update(b"stdlib-digest\0");
hasher.update(embedded_stdlib_digest());
hasher.update(b"\0");
hasher.update(b"codegen-fingerprint\0");
hasher.update(codegen_fingerprint.as_bytes());
hasher.update(b"\0");
}
fn hash_import_node(hasher: &mut Sha256, node: &ImportNode) {
match node {
ImportNode::Resolved { content } => {
hasher.update(b"resolved\0");
hasher.update(content.as_bytes());
}
ImportNode::Unresolved { import } => {
hasher.update(b"unresolved\0");
hasher.update(import.as_bytes());
}
ImportNode::IoError { kind } => {
hasher.update(b"ioerror\0");
hasher.update(kind.as_bytes());
}
}
}
enum ImportNode {
Resolved { content: Arc<str> },
Unresolved { import: Arc<str> },
IoError { kind: String },
}
#[cfg(test)]
#[path = "bytecode_cache_tests.rs"]
mod tests;