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