1use std::collections::{BTreeMap, HashSet};
2use std::future::Future;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::sync::{Arc, Mutex, OnceLock};
7
8use crate::bytecode_cache;
9use crate::module_artifact::compile_module_artifact_from_source;
10use crate::prepared_module::PreparedModuleArtifact;
11use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
12
13use super::{ScopeSpan, Vm};
14
15static STDLIB_MODULE_ARTIFACT_CACHE: OnceLock<
16 Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>>,
17> = OnceLock::new();
18
19fn stdlib_module_artifact_cache() -> &'static Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>> {
20 STDLIB_MODULE_ARTIFACT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
21}
22
23fn verified_package_source(bytes: Vec<u8>, path: &Path) -> Result<String, VmError> {
24 String::from_utf8(bytes).map_err(|error| {
25 VmError::Runtime(format!(
26 "installed package source {} is not valid UTF-8: {error}",
27 path.display()
28 ))
29 })
30}
31
32#[cfg(test)]
33fn reset_stdlib_module_artifact_cache() {
34 stdlib_module_artifact_cache().lock().unwrap().clear();
35}
36
37#[cfg(test)]
38fn stdlib_module_artifact_cache_ptr(module: &str, source: &str) -> Option<usize> {
39 let key = stdlib_artifact_cache_key(module, source);
40 stdlib_module_artifact_cache()
41 .lock()
42 .unwrap()
43 .get(&key)
44 .map(|artifact| Arc::as_ptr(artifact) as usize)
45}
46
47pub(crate) struct LoadedModule {
48 pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
49 pub(crate) public_names: HashSet<String>,
50 pub(crate) public_values: BTreeMap<String, VmValue>,
55 pub(crate) public_type_names: HashSet<String>,
58 pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
62 pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
63 pub(crate) _module_state: crate::value::ModuleState,
64}
65
66pub(crate) type ModuleCache = Arc<BTreeMap<PathBuf, Arc<LoadedModule>>>;
74
75#[derive(Clone, Debug)]
81pub(crate) struct DeferredCyclicImport {
82 pub(crate) importer: PathBuf,
84 pub(crate) target: PathBuf,
86 pub(crate) selected_names: Option<Vec<String>>,
88}
89
90pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
91 let synthetic_current_file = base.join("__harn_import_base__.harn");
92 if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
93 return resolved;
94 }
95
96 let mut file_path = base.join(path);
97
98 if !file_path.exists() && file_path.extension().is_none() {
99 file_path.set_extension("harn");
100 }
101
102 file_path
103}
104
105fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
106 let mut hasher = std::collections::hash_map::DefaultHasher::new();
107 module.hash(&mut hasher);
108 source.hash(&mut hasher);
109 format!("{module}:{:016x}", hasher.finish())
110}
111
112fn stdlib_module_artifact(
113 module: &str,
114 synthetic: &Path,
115 source: &'static str,
116 recorder: Option<&super::ModulePhaseRecorder>,
117) -> Result<Arc<PreparedModuleArtifact>, VmError> {
118 let key = stdlib_artifact_cache_key(module, source);
119 {
120 let cache = stdlib_module_artifact_cache().lock().unwrap();
121 if let Some(cached) = cache.get(&key) {
122 return Ok(Arc::clone(cached));
123 }
124 }
125
126 let lookup = {
131 let _load_span = recorder.map(super::ModulePhaseRecorder::load_span);
132 bytecode_cache::load_module(synthetic, source)
133 };
134 let artifact = if let Some(artifact) = lookup.artifact {
135 artifact
136 } else {
137 let mut compile_span = recorder.map(super::ModulePhaseRecorder::compile_span);
138 let compiled = compile_module_artifact_from_source(synthetic, source)?;
139 if let Some(span) = &mut compile_span {
140 span.mark_compile_succeeded();
141 }
142 drop(compile_span);
143 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
144 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
145 eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
146 }
147 }
148 compiled
149 };
150
151 let compiled = {
152 let _load_span = recorder.map(super::ModulePhaseRecorder::load_span);
153 Arc::new(PreparedModuleArtifact::from_cached(artifact))
154 };
155 let mut cache = stdlib_module_artifact_cache().lock().unwrap();
156 if let Some(cached) = cache.get(&key) {
157 return Ok(Arc::clone(cached));
158 }
159 cache.insert(key, Arc::clone(&compiled));
160 Ok(compiled)
161}
162
163impl Vm {
164 fn resolve_module_import_path(&self, base: &Path, path: &str) -> Result<PathBuf, VmError> {
165 if let Some(guard) = &self.package_execution_guard {
166 let synthetic_current_file = base.join("__harn_import_base__.harn");
167 if let Some(resolved) =
168 harn_modules::resolve_import_path_with_guard(&synthetic_current_file, path, guard)
169 .map_err(|error| {
170 VmError::Runtime(format!("installed package import rejected: {error}"))
171 })?
172 {
173 return Ok(resolved);
174 }
175 let mut file_path = base.join(path);
176 if !file_path.exists() && file_path.extension().is_none() {
177 file_path.set_extension("harn");
178 }
179 return Ok(file_path);
180 }
181 Ok(resolve_module_import_path(base, path))
182 }
183
184 pub async fn resolve_callable(
188 &mut self,
189 callable: &crate::value::VmCallable,
190 ) -> Result<Arc<crate::value::VmClosure>, VmError> {
191 self.ensure_execution_available()?;
192 match callable {
193 crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
194 crate::value::VmCallable::Lazy(lazy) => {
195 let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
196 let resolution = {
197 let mut modules = self.lazy_callable_modules.lock();
198 Arc::clone(
199 modules
200 .entry(cache_key)
201 .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new())),
202 )
203 };
204 let resolved = resolution
205 .get_or_try_init(|| async {
206 let exports = self.load_module_exports(&module_path).await?;
207 let exports = exports
208 .into_iter()
209 .map(|(name, closure)| (name, closure.retained_for_host_registry()))
210 .collect();
211 Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
216 exports,
217 retained_module_graph: Arc::clone(&self.module_cache),
218 }))
219 })
220 .await?;
221 resolved
222 .exports
223 .get(&lazy.function_name)
224 .cloned()
225 .ok_or_else(|| {
226 VmError::Runtime(format!(
227 "function '{}' is not exported by module '{}'",
228 lazy.function_name,
229 lazy.module_path.display()
230 ))
231 })
232 }
233 crate::value::VmCallable::Pipeline(_) => Err(VmError::TypeError(
234 "pipeline callable requires execute_callable".to_string(),
235 )),
236 }
237 }
238
239 pub async fn execute_callable(
240 &mut self,
241 callable: &crate::value::VmCallable,
242 args: &[crate::value::VmValue],
243 ) -> Result<crate::value::VmValue, VmError> {
244 let crate::value::VmCallable::Pipeline(pipeline) = callable else {
245 let closure = self.resolve_callable(callable).await?;
246 return self.call_closure_pub(&closure, args).await;
247 };
248
249 let (_, module_path) = self.lazy_module_path(&pipeline.module_path);
250 let next_guard = pipeline
251 .package_execution_guard_handle()
252 .or_else(|| self.package_execution_guard.clone());
253 let source = if let Some(guard) = &next_guard {
254 let bytes = guard.verify_entry_source(&module_path).map_err(|error| {
255 VmError::Runtime(format!("installed package execution rejected: {error}"))
256 })?;
257 verified_package_source(bytes, &module_path)?
258 } else {
259 std::fs::read_to_string(&module_path).map_err(|error| {
260 VmError::Runtime(format!("failed to read {}: {error}", module_path.display()))
261 })?
262 };
263 let program = harn_parser::check_source_strict(&source)
264 .map_err(|error| VmError::Runtime(error.to_string()))?;
265 let params = program
266 .iter()
267 .find_map(|node| {
268 let (_, inner) = harn_parser::peel_attributes(node);
269 match &inner.node {
270 harn_parser::Node::Pipeline {
271 name,
272 params,
273 is_pub: true,
274 ..
275 } if name == &pipeline.pipeline_name => Some(params.clone()),
276 _ => None,
277 }
278 })
279 .ok_or_else(|| {
280 VmError::Runtime(format!(
281 "pipeline '{}' is not exported by module '{}'",
282 pipeline.pipeline_name,
283 module_path.display()
284 ))
285 })?;
286 if params.len() != args.len() {
287 return Err(VmError::Runtime(format!(
288 "pipeline '{}' expects {} argument(s), got {}",
289 pipeline.pipeline_name,
290 params.len(),
291 args.len()
292 )));
293 }
294 let chunk = crate::Compiler::new()
295 .compile_named_with_param_globals(&program, &pipeline.pipeline_name)
296 .map_err(|error| VmError::Runtime(error.to_string()))?;
297 let previous_source_dir = self.source_dir.clone();
298 if let Some(parent) = module_path.parent() {
299 self.set_source_dir(parent);
300 }
301 for (param, value) in params.iter().zip(args) {
302 self.set_global(param, value.clone());
303 }
304 let previous_package_execution_guard =
305 std::mem::replace(&mut self.package_execution_guard, next_guard);
306 let result = self.execute(&chunk).await;
307 self.package_execution_guard = previous_package_execution_guard;
308 self.source_dir = previous_source_dir;
309 crate::stdlib::set_thread_source_dir_option(self.source_dir.as_deref());
310 result
311 }
312
313 fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
314 self.lazy_module_path(&lazy.module_path)
315 }
316
317 fn lazy_module_path(&self, path: &std::path::Path) -> (PathBuf, PathBuf) {
318 let mut module_path = if path.is_absolute() {
319 path.to_path_buf()
320 } else {
321 self.source_dir
322 .clone()
323 .unwrap_or_else(|| PathBuf::from("."))
324 .join(path)
325 };
326 if !module_path.exists() && module_path.extension().is_none() {
327 module_path.set_extension("harn");
328 }
329 let cache_key = module_path
330 .canonicalize()
331 .unwrap_or_else(|_| module_path.clone());
332 (cache_key, module_path)
333 }
334
335 async fn load_module_from_source(
336 &mut self,
337 synthetic: PathBuf,
338 source: &str,
339 ) -> Result<Arc<LoadedModule>, VmError> {
340 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
341 return Ok(loaded);
342 }
343 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
344
345 let mut compile_span = self.module_compile_span();
346 let compiled = compile_module_artifact_from_source(&synthetic, source)?;
347 if let Some(span) = &mut compile_span {
348 span.mark_compile_succeeded();
349 }
350 drop(compile_span);
351 let artifact = {
352 let _load_span = self.module_load_span();
353 PreparedModuleArtifact::from_cached(compiled)
354 };
355
356 self.imported_paths.push(synthetic.clone());
357 let loaded = Arc::new(self.instantiate_module(None, &artifact).await?);
358 self.imported_paths.pop();
359 {
360 let _load_span = self.module_load_span();
361 Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
362 }
363 self.record_module_loaded();
364 Ok(loaded)
365 }
366
367 async fn load_stdlib_module_from_source(
368 &mut self,
369 module: &str,
370 synthetic: PathBuf,
371 source: &'static str,
372 ) -> Result<Arc<LoadedModule>, VmError> {
373 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
374 return Ok(loaded);
375 }
376 Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
377
378 let artifact = stdlib_module_artifact(
379 module,
380 &synthetic,
381 source,
382 self.module_phase_recorder.as_ref(),
383 )?;
384 self.imported_paths.push(synthetic.clone());
385 let loaded = Arc::new(self.instantiate_stdlib_module(artifact.as_ref()).await?);
386 self.imported_paths.pop();
387 {
388 let _load_span = self.module_load_span();
389 Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
390 }
391 self.record_module_loaded();
392 Ok(loaded)
393 }
394
395 async fn instantiate_stdlib_module(
396 &mut self,
397 artifact: &PreparedModuleArtifact,
398 ) -> Result<LoadedModule, VmError> {
399 self.instantiate_module(None, artifact).await
400 }
401
402 async fn instantiate_module(
410 &mut self,
411 module_source_dir: Option<PathBuf>,
412 artifact: &PreparedModuleArtifact,
413 ) -> Result<LoadedModule, VmError> {
414 let caller_env = self.env.clone();
415 let old_source_dir = self.source_dir.clone();
416 self.env = VmEnv::new();
417 self.source_dir = module_source_dir.clone();
418
419 for import in &artifact.imports {
420 self.execute_import(&import.path, import.selected_names.as_deref())
421 .await?;
422 }
423
424 let _load_span = self.module_load_span();
427
428 let module_state: crate::value::ModuleState = {
429 let mut init_env = self.env.clone();
430 if let Some(init_chunk) = &artifact.init_chunk {
431 let saved_env = std::mem::replace(&mut self.env, init_env);
432 let saved_frames = std::mem::take(&mut self.frames);
433 let saved_handlers = std::mem::take(&mut self.exception_handlers);
434 let saved_iterators = std::mem::take(&mut self.iterators);
435 let saved_deadlines = std::mem::take(&mut self.deadlines);
436 let active_context = crate::step_runtime::take_active_context();
447 let init_result = self.run_chunk(Arc::clone(init_chunk)).await;
448 crate::step_runtime::restore_active_context(active_context);
449 init_env = std::mem::replace(&mut self.env, saved_env);
450 self.frames = saved_frames;
451 self.exception_handlers = saved_handlers;
452 self.iterators = saved_iterators;
453 self.deadlines = saved_deadlines;
454 init_result?;
455 }
456 Arc::new(crate::value::VmMutex::new(init_env))
457 };
458
459 let module_env = self.env.clone();
460 let registry: ModuleFunctionRegistry =
461 Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
462 let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
463 let mut public_names = artifact.public_names.clone();
464 let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
470 {
471 let state = module_state.lock();
472 for name in &artifact.public_value_names {
473 if let Some(value) = state.get(name) {
474 public_values.insert(name.clone(), value);
475 public_names.insert(name.clone());
476 }
477 }
478 }
479 let mut public_type_names = artifact.public_type_names.clone();
480 let mut public_type_schemas: BTreeMap<String, VmValue> = artifact
481 .public_type_schemas
482 .iter()
483 .filter_map(|(name, json)| {
484 let parsed = serde_json::from_str::<serde_json::Value>(json).ok()?;
485 Some((name.clone(), crate::schema::json_to_vm_value(&parsed)))
486 })
487 .collect();
488
489 for (name, compiled) in &artifact.functions {
490 let closure = Arc::new(VmClosure {
491 func: Arc::clone(compiled),
492 env: module_env.clone(),
493 source_dir: module_source_dir.clone(),
494 module_functions: Some(Arc::downgrade(®istry)),
495 module_state: Some(Arc::downgrade(&module_state)),
496 retained_module_scope: None,
497 });
498 registry.lock().insert(name.clone(), Arc::clone(&closure));
499 self.env
500 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
501 module_state
502 .lock()
503 .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
504 functions.insert(name.clone(), Arc::clone(&closure));
505 }
506
507 for import in artifact.imports.iter().filter(|import| import.is_pub) {
508 let cache_key = self.cache_key_for_import(&import.path)?;
509 let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
510 if self.imported_paths.contains(&cache_key) {
517 return Err(VmError::Runtime(format!(
518 "Re-export error: cannot `pub import` from '{}' because it forms an \
519 import cycle with this module (its public surface is still being \
520 built). Use a plain `import` here, or re-export from a module that is \
521 not part of the cycle.",
522 import.path
523 )));
524 }
525 return Err(VmError::Runtime(format!(
526 "Re-export error: imported module '{}' was not loaded",
527 import.path
528 )));
529 };
530 let names_to_reexport: Vec<String> = match &import.selected_names {
531 Some(names) => names.clone(),
532 None => loaded
536 .public_names
537 .iter()
538 .chain(loaded.public_type_names.iter())
539 .cloned()
540 .collect(),
541 };
542 for name in names_to_reexport {
543 let Some(closure) = loaded.functions.get(&name) else {
544 if let Some(value) = loaded.public_values.get(&name) {
547 public_values.insert(name.clone(), value.clone());
548 public_names.insert(name);
549 continue;
550 }
551 if loaded.public_type_names.contains(&name) {
555 if let Some(schema) = loaded.public_type_schemas.get(&name) {
556 public_type_schemas.insert(name.clone(), schema.clone());
557 }
558 public_type_names.insert(name);
559 continue;
560 }
561 return Err(VmError::Runtime(format!(
562 "Re-export error: '{name}' is not exported by '{}'",
563 import.path
564 )));
565 };
566 if let Some(existing) = functions.get(&name) {
567 if !Arc::ptr_eq(existing, closure) {
568 return Err(VmError::Runtime(format!(
569 "Re-export collision: '{name}' is defined here and also \
570 re-exported from '{}'",
571 import.path
572 )));
573 }
574 }
575 functions.insert(name.clone(), Arc::clone(closure));
576 public_names.insert(name);
577 }
578 }
579
580 self.env = caller_env;
581 self.source_dir = old_source_dir;
582
583 Ok(LoadedModule {
584 functions,
585 public_names,
586 public_values,
587 public_type_names,
588 public_type_schemas,
589 _module_functions: registry,
590 _module_state: module_state,
591 })
592 }
593
594 fn export_loaded_module(
595 &mut self,
596 module_path: &Path,
597 loaded: &LoadedModule,
598 selected_names: Option<&[String]>,
599 ) -> Result<(), VmError> {
600 let module_name = module_path.display().to_string();
601 let export_names: Vec<String> = if let Some(names) = selected_names {
602 for name in names {
608 if !loaded.public_names.contains(name) && !loaded.public_type_names.contains(name) {
609 let hint = if loaded.functions.contains_key(name) {
610 " — it is defined there but not `pub`; mark it `pub` to export it"
611 } else {
612 ""
613 };
614 return Err(VmError::Runtime(format!(
615 "Import error: '{name}' is not exported by {module_name}{hint}"
616 )));
617 }
618 }
619 names.to_vec()
620 } else {
621 loaded
624 .public_names
625 .iter()
626 .chain(loaded.public_type_names.iter())
627 .cloned()
628 .collect()
629 };
630
631 for name in export_names {
632 if loaded.public_type_names.contains(&name) && !loaded.functions.contains_key(&name) {
638 if let Some(schema) = loaded.public_type_schemas.get(&name) {
639 self.env.define(&name, schema.clone(), false)?;
640 }
641 continue;
642 }
643 if let Some(value) = loaded.public_values.get(&name) {
645 if self.env.get(&name).is_some() {
646 return Err(VmError::Runtime(format!(
647 "Import collision: '{name}' is already defined when importing \
648 {module_name}. Use selective imports to disambiguate: \
649 import {{ {name} }} from \"...\""
650 )));
651 }
652 self.env.define(&name, value.clone(), false)?;
653 continue;
654 }
655 let Some(closure) = loaded.functions.get(&name) else {
656 return Err(VmError::Runtime(format!(
657 "Import error: '{name}' is not defined in {module_name}"
658 )));
659 };
660 if let Some(VmValue::Closure(_)) = self.env.get(&name) {
661 return Err(VmError::Runtime(format!(
662 "Import collision: '{name}' is already defined when importing {module_name}. \
663 Use selective imports to disambiguate: import {{ {name} }} from \"...\""
664 )));
665 }
666 self.env
667 .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
668 }
669 Ok(())
670 }
671
672 pub(super) fn execute_import<'a>(
674 &'a mut self,
675 path: &'a str,
676 selected_names: Option<&'a [String]>,
677 ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
678 Box::pin(async move {
679 let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
680
681 let stdlib_module = path
682 .strip_prefix("std/")
683 .or_else(|| (path == "observability").then_some("observability"));
684 if let Some(module) = stdlib_module {
685 if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
686 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
687 if self.imported_paths.contains(&synthetic) {
688 return Ok(());
689 }
690 if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
691 return self.export_loaded_module(&synthetic, &loaded, selected_names);
692 }
693 let loaded = self
694 .load_stdlib_module_from_source(module, synthetic.clone(), source)
695 .await?;
696 {
697 let _load_span = self.module_load_span();
698 self.export_loaded_module(&synthetic, &loaded, selected_names)?;
699 }
700 return Ok(());
701 }
702 return Err(VmError::Runtime(format!(
703 "Unknown stdlib module: std/{module}"
704 )));
705 }
706
707 let base = self
708 .source_dir
709 .clone()
710 .unwrap_or_else(|| PathBuf::from("."));
711 let file_path = self.resolve_module_import_path(&base, path)?;
712 let verified_source = if let Some(guard) = &self.package_execution_guard {
713 let bytes = guard.verify_entry_source(&file_path).map_err(|error| {
714 VmError::Runtime(format!("installed package import rejected: {error}"))
715 })?;
716 Some(verified_package_source(bytes, &file_path)?)
717 } else {
718 None
719 };
720
721 let canonical = file_path
722 .canonicalize()
723 .unwrap_or_else(|_| file_path.clone());
724 if self.imported_paths.contains(&canonical) {
725 if let Some(importer) = self.imported_paths.last().cloned() {
733 if importer != canonical {
734 self.deferred_cyclic_imports.push(DeferredCyclicImport {
735 importer,
736 target: canonical.clone(),
737 selected_names: selected_names.map(<[String]>::to_vec),
738 });
739 }
740 }
741 return Ok(());
742 }
743 if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
744 if let Some(source) = &verified_source {
745 let cached_source = self.source_cache.get(&canonical);
746 if cached_source != Some(source) {
747 return Err(VmError::Runtime(format!(
748 "installed package import rejected: cached module {} was not compiled from the verified package bytes",
749 canonical.display()
750 )));
751 }
752 }
753 return self.export_loaded_module(&canonical, &loaded, selected_names);
754 }
755 self.imported_paths.push(canonical.clone());
756
757 let source = {
758 let _load_span = self.module_load_span();
759 match verified_source {
760 Some(source) => source,
761 None => std::fs::read_to_string(&file_path).map_err(|e| {
762 VmError::Runtime(format!(
767 "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
768 file_path.display(),
769 base.display()
770 ))
771 })?,
772 }
773 };
774 Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
775 Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
776
777 let prepared = {
778 let _load_span = self.module_load_span();
779 if bytecode_cache::cache_enabled() {
780 self.prepared_module_cache.get(&canonical, &source)
781 } else {
782 None
783 }
784 };
785 let artifact = if let Some(prepared) = prepared {
786 prepared
787 } else {
788 let lookup = {
792 let _load_span = self.module_load_span();
793 bytecode_cache::load_module(&file_path, &source)
794 };
795 let cached = if let Some(artifact) = lookup.artifact {
796 artifact
797 } else {
798 let mut compile_span = self.module_compile_span();
799 let compiled = compile_module_artifact_from_source(&file_path, &source)?;
800 if let Some(span) = &mut compile_span {
801 span.mark_compile_succeeded();
802 }
803 drop(compile_span);
804 if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
805 if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
806 eprintln!(
807 "[harn] module cache write skipped for {}: {err}",
808 file_path.display()
809 );
810 }
811 }
812 compiled
813 };
814 let mut prepared = {
815 let _load_span = self.module_load_span();
816 Arc::new(PreparedModuleArtifact::from_cached(cached))
817 };
818 if bytecode_cache::cache_enabled() {
819 prepared =
820 self.prepared_module_cache
821 .insert(canonical.clone(), &source, prepared);
822 }
823 prepared
824 };
825
826 let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
827 let loaded = Arc::new(
828 self.instantiate_module(module_source_dir, artifact.as_ref())
829 .await?,
830 );
831 self.imported_paths.pop();
832 {
833 let _load_span = self.module_load_span();
834 Arc::make_mut(&mut self.module_cache)
835 .insert(canonical.clone(), Arc::clone(&loaded));
836 }
837 self.record_module_loaded();
838 {
839 let _load_span = self.module_load_span();
840 self.export_loaded_module(&canonical, &loaded, selected_names)?;
841 }
842
843 if self.imported_paths.is_empty() {
847 let _load_span = self.module_load_span();
848 self.flush_deferred_cyclic_imports()?;
849 }
850
851 Ok(())
852 })
853 }
854
855 fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
864 if self.deferred_cyclic_imports.is_empty() {
865 return Ok(());
866 }
867 let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
868 let mut still_pending = Vec::new();
869 for import in deferred {
870 let (Some(importer), Some(target)) = (
871 self.module_cache.get(&import.importer).cloned(),
872 self.module_cache.get(&import.target).cloned(),
873 ) else {
874 still_pending.push(import);
878 continue;
879 };
880
881 let export_names: Vec<String> = match &import.selected_names {
882 Some(names) => names.clone(),
883 None if !target.public_names.is_empty() => {
884 target.public_names.iter().cloned().collect()
885 }
886 None => target.functions.keys().cloned().collect(),
887 };
888
889 let mut module_state = importer._module_state.lock();
890 for name in export_names {
891 if module_state.get(&name).is_some() {
894 continue;
895 }
896 if let Some(closure) = target.functions.get(&name) {
897 module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
898 } else if let Some(value) = target.public_values.get(&name) {
899 module_state.define(&name, value.clone(), false)?;
901 } else {
902 return Err(VmError::Runtime(format!(
903 "Import error: '{name}' is not defined in {}",
904 import.target.display()
905 )));
906 }
907 }
908 }
909 self.deferred_cyclic_imports = still_pending;
910 Ok(())
911 }
912
913 fn cache_key_for_import(&self, path: &str) -> Result<PathBuf, VmError> {
918 if let Some(module) = path
919 .strip_prefix("std/")
920 .or_else(|| (path == "observability").then_some("observability"))
921 {
922 return Ok(PathBuf::from(format!("<stdlib>/{module}.harn")));
923 }
924 let base = self
925 .source_dir
926 .clone()
927 .unwrap_or_else(|| PathBuf::from("."));
928 let file_path = self.resolve_module_import_path(&base, path)?;
929 Ok(file_path.canonicalize().unwrap_or(file_path))
930 }
931
932 pub async fn load_module_exports(
935 &mut self,
936 path: &Path,
937 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
938 self.ensure_execution_available()?;
939 let path_str = path.to_string_lossy().into_owned();
940 self.execute_import(&path_str, None).await?;
941
942 let mut file_path = if path.is_absolute() {
943 path.to_path_buf()
944 } else {
945 self.source_dir
946 .clone()
947 .unwrap_or_else(|| PathBuf::from("."))
948 .join(path)
949 };
950 if !file_path.exists() && file_path.extension().is_none() {
951 file_path.set_extension("harn");
952 }
953
954 let canonical = file_path
955 .canonicalize()
956 .unwrap_or_else(|_| file_path.clone());
957 let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
958 VmError::Runtime(format!(
959 "Import error: failed to cache loaded module '{}'",
960 canonical.display()
961 ))
962 })?;
963
964 let export_names: Vec<String> = if loaded.public_names.is_empty() {
965 loaded.functions.keys().cloned().collect()
966 } else {
967 loaded.public_names.iter().cloned().collect()
968 };
969
970 let mut exports = BTreeMap::new();
971 for name in export_names {
972 let Some(closure) = loaded.functions.get(&name) else {
973 return Err(VmError::Runtime(format!(
974 "Import error: exported function '{name}' is missing from {}",
975 canonical.display()
976 )));
977 };
978 exports.insert(name, Arc::clone(closure));
979 }
980
981 Ok(exports)
982 }
983
984 pub async fn load_module_exports_from_source(
987 &mut self,
988 source_key: impl Into<PathBuf>,
989 source: &str,
990 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
991 self.ensure_execution_available()?;
992 let synthetic = source_key.into();
993 let loaded = self
994 .load_module_from_source(synthetic.clone(), source)
995 .await?;
996 let export_names: Vec<String> = if loaded.public_names.is_empty() {
997 loaded.functions.keys().cloned().collect()
998 } else {
999 loaded.public_names.iter().cloned().collect()
1000 };
1001
1002 let mut exports = BTreeMap::new();
1003 for name in export_names {
1004 let Some(closure) = loaded.functions.get(&name) else {
1005 return Err(VmError::Runtime(format!(
1006 "Import error: exported function '{name}' is missing from {}",
1007 synthetic.display()
1008 )));
1009 };
1010 exports.insert(name, Arc::clone(closure));
1011 }
1012
1013 Ok(exports)
1014 }
1015
1016 pub async fn load_module_exports_from_import(
1020 &mut self,
1021 import_path: &str,
1022 ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1023 self.ensure_execution_available()?;
1024 self.execute_import(import_path, None).await?;
1025
1026 if let Some(module) = import_path
1027 .strip_prefix("std/")
1028 .or_else(|| (import_path == "observability").then_some("observability"))
1029 {
1030 let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
1031 let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
1032 VmError::Runtime(format!(
1033 "Import error: failed to cache loaded module '{}'",
1034 synthetic.display()
1035 ))
1036 })?;
1037 let mut exports = BTreeMap::new();
1038 let export_names: Vec<String> = if loaded.public_names.is_empty() {
1039 loaded.functions.keys().cloned().collect()
1040 } else {
1041 loaded.public_names.iter().cloned().collect()
1042 };
1043 for name in export_names {
1044 let Some(closure) = loaded.functions.get(&name) else {
1045 return Err(VmError::Runtime(format!(
1046 "Import error: exported function '{name}' is missing from {}",
1047 synthetic.display()
1048 )));
1049 };
1050 exports.insert(name, Arc::clone(closure));
1051 }
1052 return Ok(exports);
1053 }
1054
1055 let base = self
1056 .source_dir
1057 .clone()
1058 .unwrap_or_else(|| PathBuf::from("."));
1059 let file_path = self.resolve_module_import_path(&base, import_path)?;
1060 self.load_module_exports(&file_path).await
1061 }
1062}
1063
1064#[cfg(test)]
1065#[path = "modules_tests.rs"]
1066mod tests;