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