1use std::collections::{BTreeMap, VecDeque};
8use std::num::NonZeroUsize;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use harn_modules::DefKind;
13use parking_lot::Mutex;
14
15use crate::chunk::{Chunk, CompiledFunction};
16use crate::module_artifact::{
17 compile_module_artifact_from_source, compile_module_artifact_from_source_with_imported_enums,
18 ModuleArtifact, ModuleImportSpec, ModuleProvenance,
19};
20use crate::module_source::ModuleSource;
21use crate::{ModulePhaseRecorder, ModulePhaseStats, VmError};
22const DEFAULT_MAX_ENTRIES: usize = 512;
23
24pub(crate) struct PreparedModuleArtifact {
26 pub(crate) provenance: ModuleProvenance,
27 pub(crate) imports: Vec<ModuleImportSpec>,
28 pub(crate) type_schema_init_chunks: Vec<Arc<Chunk>>,
29 pub(crate) init_chunk: Option<Arc<Chunk>>,
30 pub(crate) functions: BTreeMap<String, Arc<CompiledFunction>>,
31 pub(crate) public_exports: BTreeMap<String, DefKind>,
32 pub(crate) public_value_names: std::collections::HashSet<String>,
33 pub(crate) public_type_names: std::collections::HashSet<String>,
34}
35
36impl PreparedModuleArtifact {
37 pub(crate) fn from_cached(artifact: ModuleArtifact) -> Self {
38 let ModuleArtifact {
39 provenance,
40 imports,
41 type_schema_init_chunks,
42 init_chunk,
43 functions,
44 public_exports,
45 public_value_names,
46 public_type_names,
47 } = artifact;
48 let type_schema_init_chunks = type_schema_init_chunks
49 .into_iter()
50 .map(|chunk| Arc::new(Chunk::from_cached(chunk)))
51 .collect();
52 let init_chunk = init_chunk.map(|chunk| Arc::new(Chunk::from_cached(chunk)));
53 let functions = functions
54 .into_iter()
55 .map(|(name, function)| (name, Arc::new(CompiledFunction::from_cached(function))))
56 .collect();
57 Self {
58 provenance,
59 imports,
60 type_schema_init_chunks,
61 init_chunk,
62 functions,
63 public_exports,
64 public_value_names,
65 public_type_names,
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
71struct PreparedModuleCacheKey {
72 canonical_path: PathBuf,
73 source_hash: [u8; 32],
74 provenance: ModuleProvenance,
75 harn_version: &'static str,
76 codegen_fingerprint: &'static str,
77 optimizations_enabled: bool,
78}
79
80impl PreparedModuleCacheKey {
81 fn new(canonical_path: PathBuf, source_hash: [u8; 32], provenance: ModuleProvenance) -> Self {
86 Self {
87 canonical_path,
88 source_hash,
89 provenance,
90 harn_version: crate::bytecode_cache::HARN_VERSION,
91 codegen_fingerprint: crate::bytecode_cache::CODEGEN_FINGERPRINT,
92 optimizations_enabled: crate::compiler::CompilerOptions::from_env()
93 .optimizations_enabled(),
94 }
95 }
96}
97
98#[derive(Default)]
99struct PreparedModuleCacheInner {
100 entries: BTreeMap<PreparedModuleCacheKey, Arc<PreparedModuleArtifact>>,
101 insertion_order: VecDeque<PreparedModuleCacheKey>,
102 hits: u64,
103 misses: u64,
104 insertions: u64,
105 evictions: u64,
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
110#[non_exhaustive]
111pub struct PreparedModuleCacheStats {
112 pub hits: u64,
113 pub misses: u64,
114 pub insertions: u64,
115 pub evictions: u64,
116 pub entries: usize,
117}
118
119#[derive(Clone)]
125pub struct PreparedModuleCache {
126 max_entries: NonZeroUsize,
127 inner: Arc<Mutex<PreparedModuleCacheInner>>,
128}
129
130impl Default for PreparedModuleCache {
131 fn default() -> Self {
132 Self::with_capacity(
133 NonZeroUsize::new(DEFAULT_MAX_ENTRIES).expect("non-zero cache capacity"),
134 )
135 }
136}
137
138impl PreparedModuleCache {
139 pub fn with_capacity(max_entries: NonZeroUsize) -> Self {
140 Self {
141 max_entries,
142 inner: Arc::new(Mutex::new(PreparedModuleCacheInner::default())),
143 }
144 }
145
146 pub fn stats(&self) -> PreparedModuleCacheStats {
147 let inner = self.inner.lock();
148 PreparedModuleCacheStats {
149 hits: inner.hits,
150 misses: inner.misses,
151 insertions: inner.insertions,
152 evictions: inner.evictions,
153 entries: inner.entries.len(),
154 }
155 }
156
157 pub fn prepare_import_graph(&self, roots: &[PathBuf]) -> ModulePhaseStats {
164 if roots.is_empty() {
165 return ModulePhaseStats::default();
166 }
167
168 let graph = harn_modules::build(roots);
169 let root_paths = roots
170 .iter()
171 .map(|path| harn_modules::canonical_path(path))
172 .collect::<std::collections::HashSet<_>>();
173 let recorder = ModulePhaseRecorder::new();
174
175 for path in graph.module_paths() {
176 if root_paths.contains(&harn_modules::canonical_path(&path)) {
177 continue;
178 }
179 if path.to_str().is_some_and(|path| path.starts_with("<std>/")) {
180 let _ = crate::vm::prepare_stdlib_module_artifact(&path, Some(&recorder));
181 continue;
182 }
183
184 let source = {
185 let _load_span = recorder.load_span();
186 match crate::module_source::read(&path) {
187 Ok(source) => source,
188 Err(_) => continue,
189 }
190 };
191 let mut imported_enum_candidates = graph
192 .imported_names_by_kind_for_file(&path, DefKind::Enum)
193 .unwrap_or_default()
194 .into_iter()
195 .collect::<Vec<_>>();
196 imported_enum_candidates.sort_unstable();
197 let canonical = harn_modules::canonical_path(&path);
198 let _ = self.prepare(
199 &path,
200 &canonical,
201 &source,
202 Some(&imported_enum_candidates),
203 Some(&recorder),
204 );
205 }
206
207 recorder.snapshot()
208 }
209
210 pub(crate) fn get(
211 &self,
212 canonical_path: &Path,
213 source_hash: [u8; 32],
214 ) -> Option<Arc<PreparedModuleArtifact>> {
215 let key = PreparedModuleCacheKey::new(
216 canonical_path.to_path_buf(),
217 source_hash,
218 ModuleProvenance::User,
219 );
220 let mut inner = self.inner.lock();
221 let artifact = inner.entries.get(&key).cloned();
222 if artifact.is_some() {
223 inner.hits = inner.hits.saturating_add(1);
224 } else {
225 inner.misses = inner.misses.saturating_add(1);
226 }
227 artifact
228 }
229
230 pub(crate) fn insert(
231 &self,
232 canonical_path: PathBuf,
233 source_hash: [u8; 32],
234 artifact: Arc<PreparedModuleArtifact>,
235 ) -> Arc<PreparedModuleArtifact> {
236 let key = PreparedModuleCacheKey::new(canonical_path, source_hash, artifact.provenance);
237 let mut inner = self.inner.lock();
238 if let Some(existing) = inner.entries.get(&key) {
239 return Arc::clone(existing);
240 }
241 while inner.entries.len() >= self.max_entries.get() {
242 let Some(oldest) = inner.insertion_order.pop_front() else {
243 break;
244 };
245 if inner.entries.remove(&oldest).is_some() {
246 inner.evictions = inner.evictions.saturating_add(1);
247 }
248 }
249 inner.insertion_order.push_back(key.clone());
250 inner.entries.insert(key, Arc::clone(&artifact));
251 inner.insertions = inner.insertions.saturating_add(1);
252 artifact
253 }
254
255 pub(crate) fn prepare(
256 &self,
257 source_path: &Path,
258 canonical_path: &Path,
259 source: &ModuleSource,
260 imported_enum_candidates: Option<&[String]>,
261 recorder: Option<&ModulePhaseRecorder>,
262 ) -> Result<Arc<PreparedModuleArtifact>, VmError> {
263 let prepared = {
264 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
265 self.get(canonical_path, source.sha256())
266 };
267 if let Some(prepared) = prepared {
268 return Ok(prepared);
269 }
270
271 let lookup = {
275 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
276 crate::bytecode_cache::load_module(source_path, source)
277 };
278 let cached = if let Some(artifact) = lookup.artifact {
279 artifact
280 } else {
281 let mut compile_span = recorder.map(ModulePhaseRecorder::compile_span);
282 let compiled = match imported_enum_candidates {
283 Some(candidates) => compile_module_artifact_from_source_with_imported_enums(
284 source_path,
285 source.as_str(),
286 candidates.iter().cloned(),
287 )?,
288 None => compile_module_artifact_from_source(source_path, source.as_str())?,
289 };
290 if let Some(span) = &mut compile_span {
291 span.mark_compile_succeeded();
292 }
293 drop(compile_span);
294 if let Err(err) = crate::bytecode_cache::store_module(&lookup.key, &compiled) {
295 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
296 eprintln!(
297 "[harn] module cache write skipped for {}: {err}",
298 source_path.display()
299 );
300 }
301 }
302 compiled
303 };
304 let prepared = {
305 let _load_span = recorder.map(ModulePhaseRecorder::load_span);
306 Arc::new(PreparedModuleArtifact::from_cached(cached))
307 };
308 Ok(self.insert(canonical_path.to_path_buf(), source.sha256(), prepared))
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::module_artifact::{compile_module_artifact_from_source, ModuleImportBinding};
316 use crate::module_source::ModuleSource;
317 use harn_parser::TypeExpr;
318
319 fn named_list_element(type_expr: &Option<TypeExpr>) -> &str {
320 match type_expr {
321 Some(TypeExpr::List(inner)) => match inner.as_ref() {
322 TypeExpr::Named(name) => name,
323 other => panic!("expected named list element, got {other:?}"),
324 },
325 other => panic!("expected list parameter type, got {other:?}"),
326 }
327 }
328
329 fn empty_artifact_with_provenance(provenance: ModuleProvenance) -> Arc<PreparedModuleArtifact> {
330 Arc::new(PreparedModuleArtifact::from_cached(ModuleArtifact {
331 provenance,
332 imports: Vec::new(),
333 type_schema_init_chunks: Vec::new(),
334 init_chunk: None,
335 functions: BTreeMap::new(),
336 public_exports: BTreeMap::new(),
337 public_value_names: Default::default(),
338 public_type_names: Default::default(),
339 }))
340 }
341
342 fn empty_artifact() -> Arc<PreparedModuleArtifact> {
343 empty_artifact_with_provenance(ModuleProvenance::User)
344 }
345
346 #[test]
347 fn ordinary_lookup_cannot_reuse_privileged_wire_bytecode() {
348 let cache = PreparedModuleCache::default();
349 let source = ModuleSource::from_text("const value = 1");
350 let _ = cache.insert(
351 PathBuf::from("same.harn"),
352 source.sha256(),
353 empty_artifact_with_provenance(ModuleProvenance::PrivilegedWire),
354 );
355 assert!(
356 cache.get(Path::new("same.harn"), source.sha256()).is_none(),
357 "user module lookup must be provenance-separated"
358 );
359 }
360
361 #[test]
362 fn bounded_cache_evicts_oldest_exact_key() {
363 let cache = PreparedModuleCache::with_capacity(NonZeroUsize::new(1).unwrap());
364 let first_source = ModuleSource::from_text("pub fn first() { 1 }");
365 let second_source = ModuleSource::from_text("pub fn second() { 2 }");
366 let first = empty_artifact();
367 let _ = cache.insert(PathBuf::from("first.harn"), first_source.sha256(), first);
368 let _ = cache.insert(
369 PathBuf::from("second.harn"),
370 second_source.sha256(),
371 empty_artifact(),
372 );
373
374 assert!(cache
375 .get(Path::new("first.harn"), first_source.sha256())
376 .is_none());
377 assert!(cache
378 .get(Path::new("second.harn"), second_source.sha256())
379 .is_some());
380 assert_eq!(cache.stats().evictions, 1);
381 assert_eq!(cache.stats().entries, 1);
382 }
383
384 #[test]
385 fn cache_key_separates_compiler_configuration() {
386 let path = PathBuf::from("module.harn");
387 let key = PreparedModuleCacheKey::new(
388 path,
389 ModuleSource::from_text("pub fn value() { 1 }").sha256(),
390 ModuleProvenance::User,
391 );
392 let mut other_compiler = key.clone();
393 other_compiler.optimizations_enabled = !key.optimizations_enabled;
394
395 assert_ne!(key, other_compiler);
396 }
397
398 #[test]
399 fn dropping_last_cache_handle_releases_prepared_artifacts() {
400 let cache = PreparedModuleCache::default();
401 let path = PathBuf::from("module.harn");
402 let source = ModuleSource::from_text("pub fn value() { 1 }");
403 let artifact = empty_artifact();
404 let weak = Arc::downgrade(&artifact);
405 let _ = cache.insert(path, source.sha256(), artifact);
406 let clone = cache.clone();
407
408 drop(cache);
409 assert!(weak.upgrade().is_some());
410 drop(clone);
411 assert!(weak.upgrade().is_none());
412 }
413
414 #[test]
415 fn hydration_moves_module_owned_storage() {
416 let source = r#"
417import { assert_eq } from "std/testing"
418pub type Result = {value: int}
419pub const value = 1
420pub fn answer(items: list<string>) {
421 fn nested() { return 42 }
422 return items
423}
424"#;
425 let artifact = compile_module_artifact_from_source(Path::new("owned.harn"), source)
426 .expect("compile typed module artifact");
427
428 let imports = artifact.imports.as_ptr();
429 let import_path = artifact.imports[0].path.as_ptr();
430 let ModuleImportBinding::Selected(selected) = &artifact.imports[0].binding else {
431 panic!("expected selective import");
432 };
433 let selected_names = selected.as_ptr();
434 let selected_name = selected[0].as_ptr();
435 let init_code = artifact.init_chunk.as_ref().unwrap().code.as_ptr();
436 let schema_init_codes = artifact
437 .type_schema_init_chunks
438 .iter()
439 .map(|chunk| chunk.code.as_ptr())
440 .collect::<Vec<_>>();
441 let (function_key, function) = artifact.functions.first_key_value().unwrap();
442 let function_key = function_key.as_ptr();
443 let function_name = function.name.as_ptr();
444 let function_code = function.chunk.code.as_ptr();
445 let param_name = function.params[0].name.as_ptr();
446 let param_type_name = named_list_element(&function.params[0].type_expr).as_ptr();
447 let nested_name = function.chunk.functions[0].name.as_ptr();
448 let nested_code = function.chunk.functions[0].chunk.code.as_ptr();
449 let public_export_name = artifact
450 .public_exports
451 .get_key_value("answer")
452 .unwrap()
453 .0
454 .as_ptr();
455 let public_export_kind = *artifact.public_exports.get("answer").unwrap();
456 let public_value_name = artifact.public_value_names.get("value").unwrap().as_ptr();
457 let public_type_name = artifact.public_type_names.get("Result").unwrap().as_ptr();
458 let hydrated = PreparedModuleArtifact::from_cached(artifact);
459
460 assert_eq!(hydrated.imports.as_ptr(), imports);
461 assert_eq!(hydrated.imports[0].path.as_ptr(), import_path);
462 let ModuleImportBinding::Selected(selected) = &hydrated.imports[0].binding else {
463 panic!("expected selective import");
464 };
465 assert_eq!(selected.as_ptr(), selected_names);
466 assert_eq!(selected[0].as_ptr(), selected_name);
467 assert_eq!(
468 hydrated.init_chunk.as_ref().unwrap().code.as_ptr(),
469 init_code
470 );
471 assert_eq!(
472 hydrated
473 .type_schema_init_chunks
474 .iter()
475 .map(|chunk| chunk.code.as_ptr())
476 .collect::<Vec<_>>(),
477 schema_init_codes
478 );
479 let (hydrated_function_key, hydrated_function) =
480 hydrated.functions.first_key_value().unwrap();
481 assert_eq!(hydrated_function_key.as_ptr(), function_key);
482 assert_eq!(hydrated_function.name.as_ptr(), function_name);
483 assert_eq!(hydrated_function.chunk.code.as_ptr(), function_code);
484 assert_eq!(hydrated_function.params[0].name.as_ptr(), param_name);
485 assert_eq!(
486 named_list_element(&hydrated_function.params[0].type_expr).as_ptr(),
487 param_type_name
488 );
489 assert_eq!(
490 hydrated_function.chunk.functions[0].name.as_ptr(),
491 nested_name
492 );
493 assert_eq!(
494 hydrated_function.chunk.functions[0].chunk.code.as_ptr(),
495 nested_code
496 );
497 assert_eq!(
498 hydrated
499 .public_exports
500 .get_key_value("answer")
501 .unwrap()
502 .0
503 .as_ptr(),
504 public_export_name
505 );
506 assert_eq!(
507 hydrated.public_exports.get("answer"),
508 Some(&public_export_kind)
509 );
510 assert_eq!(
511 hydrated.public_value_names.get("value").unwrap().as_ptr(),
512 public_value_name
513 );
514 assert_eq!(
515 hydrated.public_type_names.get("Result").unwrap().as_ptr(),
516 public_type_name
517 );
518 }
519}