1use std::collections::{BTreeMap, VecDeque};
8use std::num::NonZeroUsize;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use parking_lot::Mutex;
13
14use crate::chunk::{Chunk, CompiledFunction};
15use crate::module_artifact::{ModuleArtifact, ModuleImportSpec};
16
17const DEFAULT_MAX_ENTRIES: usize = 512;
18
19pub(crate) struct PreparedModuleArtifact {
21 pub(crate) imports: Vec<ModuleImportSpec>,
22 pub(crate) init_chunk: Option<Arc<Chunk>>,
23 pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
24 pub(crate) public_names: std::collections::HashSet<String>,
25 pub(crate) public_value_names: std::collections::HashSet<String>,
26 pub(crate) public_type_names: std::collections::HashSet<String>,
27 pub(crate) public_type_schemas: BTreeMap<String, String>,
28}
29
30impl PreparedModuleArtifact {
31 pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
32 let init_chunk = artifact
33 .init_chunk
34 .as_ref()
35 .map(|chunk| Arc::new(Chunk::from_cached(chunk)));
36 let functions = artifact
37 .functions
38 .iter()
39 .map(|(name, function)| {
40 (
41 name.clone(),
42 Arc::new(CompiledFunction::from_cached(function)),
43 )
44 })
45 .collect();
46 Self {
47 imports: artifact.imports,
48 init_chunk,
49 functions,
50 public_names: artifact.public_names,
51 public_value_names: artifact.public_value_names,
52 public_type_names: artifact.public_type_names,
53 public_type_schemas: artifact.public_type_schemas,
54 }
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
59struct PreparedModuleCacheKey {
60 canonical_path: PathBuf,
61 source_hash: [u8; 32],
62 harn_version: &'static str,
63 codegen_fingerprint: &'static str,
64 optimizations_enabled: bool,
65}
66
67impl PreparedModuleCacheKey {
68 fn new(canonical_path: PathBuf, source: &str) -> Self {
69 Self {
70 canonical_path,
71 source_hash: *blake3::hash(source.as_bytes()).as_bytes(),
72 harn_version: crate::bytecode_cache::HARN_VERSION,
73 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
74 optimizations_enabled: crate::compiler::CompilerOptions::from_env()
75 .optimizations_enabled(),
76 }
77 }
78}
79
80#[derive(Default)]
81struct PreparedModuleCacheInner {
82 entries: BTreeMap<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>,
83 insertion_order: VecDeque<PreparedModuleCacheKey>,
84 hits: u64,
85 misses: u64,
86 insertions: u64,
87 evictions: u64,
88}
89
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92#[non_exhaustive]
93pub struct PreparedModuleCacheStats {
94 pub hits: u64,
95 pub misses: u64,
96 pub insertions: u64,
97 pub evictions: u64,
98 pub entries: usize,
99}
100
101#[derive(Clone)]
107pub struct PreparedModuleCache {
108 max_entries: NonZeroUsize,
109 inner: Arc<Mutex<PreparedModuleCacheInner>>,
110}
111
112impl Default for PreparedModuleCache {
113 fn default() -> Self {
114 Self::with_capacity(
115 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
116 )
117 }
118}
119
120impl PreparedModuleCache {
121 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
122 Self {
123 max_entries,
124 inner: Arc::new(Mutex::new(PreparedModuleCacheInner::default())),
125 }
126 }
127
128 pub fn stats(&self) -> PreparedModuleCacheStats {
129 let inner = self.inner.lock();
130 PreparedModuleCacheStats {
131 hits: inner.hits,
132 misses: inner.misses,
133 insertions: inner.insertions,
134 evictions: inner.evictions,
135 entries: inner.entries.len(),
136 }
137 }
138
139 pub(crate) fn get(
140 &self,
141 canonical_path: &Path,
142 source: &str,
143 ) -> Option<Arc<PreparedModuleArtifact>> {
144 let key = PreparedModuleCacheKey::new(canonical_path.to_path_buf(), source);
145 let mut inner = self.inner.lock();
146 let artifact = inner.entries.get(&key).cloned();
147 if artifact.is_some() {
148 inner.hits = inner.hits.saturating_add(1);
149 } else {
150 inner.misses = inner.misses.saturating_add(1);
151 }
152 artifact
153 }
154
155 pub(crate) fn insert(
156 &self,
157 canonical_path: PathBuf,
158 source: &str,
159 artifact: Arc<PreparedModuleArtifact>,
160 ) -> Arc<PreparedModuleArtifact> {
161 let key = PreparedModuleCacheKey::new(canonical_path, source);
162 let mut inner = self.inner.lock();
163 if let Some(existing) = inner.entries.get(&key) {
164 return Arc::clone(existing);
165 }
166 while inner.entries.len() >= self.max_entries.get() {
167 let Some(oldest) = inner.insertion_order.pop_front() else {
168 break;
169 };
170 if inner.entries.remove(&oldest).is_some() {
171 inner.evictions = inner.evictions.saturating_add(1);
172 }
173 }
174 inner.insertion_order.push_back(key.clone());
175 inner.entries.insert(key, Arc::clone(&artifact));
176 inner.insertions = inner.insertions.saturating_add(1);
177 artifact
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
186 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
187 imports: Vec::new(),
188 init_chunk: None,
189 functions: BTreeMap::new(),
190 public_names: Default::default(),
191 public_value_names: Default::default(),
192 public_type_names: Default::default(),
193 public_type_schemas: BTreeMap::new(),
194 }))
195 }
196
197 #[test]
198 fn bounded_cache_evicts_oldest_exact_key() {
199 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
200 let first_source = "pub fn first() { 1 }";
201 let second_source = "pub fn second() { 2 }";
202 let first = empty_artifact();
203 let _ = cache.insert(PathBuf::from("first.harn"), first_source, first);
204 let _ = cache.insert(
205 PathBuf::from("second.harn"),
206 second_source,
207 empty_artifact(),
208 );
209
210 assert!(cache.get(Path::new("first.harn"), first_source).is_none());
211 assert!(cache.get(Path::new("second.harn"), second_source).is_some());
212 assert_eq!(cache.stats().evictions, 1);
213 assert_eq!(cache.stats().entries, 1);
214 }
215
216 #[test]
217 fn cache_key_separates_compiler_configuration() {
218 let path = PathBuf::from("module.harn");
219 let key = PreparedModuleCacheKey::new(path, "pub fn value() { 1 }");
220 let mut other_compiler = key.clone();
221 other_compiler.optimizations_enabled = !key.optimizations_enabled;
222
223 assert_ne!(key, other_compiler);
224 }
225
226 #[test]
227 fn dropping_last_cache_handle_releases_prepared_artifacts() {
228 let cache = PreparedModuleCache::default();
229 let path = PathBuf::from("module.harn");
230 let source = "pub fn value() { 1 }";
231 let artifact = empty_artifact();
232 let weak = Arc::downgrade(&artifact);
233 let _ = cache.insert(path, source, artifact);
234 let clone = cache.clone();
235
236 drop(cache);
237 assert!(weak.upgrade().is_some());
238 drop(clone);
239 assert!(weak.upgrade().is_none());
240 }
241}