Skip to main content

harn_vm/vm/
modules.rs

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
23#[cfg(test)]
24fn reset_stdlib_module_artifact_cache() {
25    stdlib_module_artifact_cache().lock().unwrap().clear();
26}
27
28#[cfg(test)]
29fn stdlib_module_artifact_cache_ptr(module: &str, source: &str) -> Option<usize> {
30    let key = stdlib_artifact_cache_key(module, source);
31    stdlib_module_artifact_cache()
32        .lock()
33        .unwrap()
34        .get(&key)
35        .map(|artifact| Arc::as_ptr(artifact) as usize)
36}
37
38#[derive(Clone)]
39pub(crate) struct LoadedModule {
40    pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
41    pub(crate) public_names: HashSet<String>,
42    /// Evaluated values of exported `pub const` / `pub let` bindings, read out
43    /// of the instantiated module env after the init chunk ran. Importers bind
44    /// these by value (like every other cross-module value). Disjoint from
45    /// `functions` and `public_type_names`.
46    pub(crate) public_values: BTreeMap<String, VmValue>,
47    /// Names of `pub type` aliases (and re-exported ones). Erased at runtime:
48    /// selective imports may name them, but they bind no value of their own.
49    pub(crate) public_type_names: HashSet<String>,
50    /// Decoded JSON-Schema dict for each `pub type` alias that lowers to a
51    /// schema. Importers bind the alias name to this value so
52    /// expression-position uses (`output_schema: ImportedAlias`) work.
53    pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
54    pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
55    pub(crate) _module_state: crate::value::ModuleState,
56}
57
58/// An import whose target module was still mid-load (an import cycle) when the
59/// importing module reached it. The target's function closures don't exist yet
60/// at that point, so the binding can't happen inline. We record it here and
61/// resolve it once both modules are fully loaded — see
62/// [`Vm::flush_deferred_cyclic_imports`].
63#[derive(Clone, Debug)]
64pub(crate) struct DeferredCyclicImport {
65    /// Canonical path of the module that issued the import.
66    pub(crate) importer: PathBuf,
67    /// Canonical path of the cyclically-imported target module.
68    pub(crate) target: PathBuf,
69    /// Selectively-imported names, or `None` for a wildcard/side-effect import.
70    pub(crate) selected_names: Option<Vec<String>>,
71}
72
73pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
74    let synthetic_current_file = base.join("__harn_import_base__.harn");
75    if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
76        return resolved;
77    }
78
79    let mut file_path = base.join(path);
80
81    if !file_path.exists() && file_path.extension().is_none() {
82        file_path.set_extension("harn");
83    }
84
85    file_path
86}
87
88fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
89    let mut hasher = std::collections::hash_map::DefaultHasher::new();
90    module.hash(&mut hasher);
91    source.hash(&mut hasher);
92    format!("{module}:{:016x}", hasher.finish())
93}
94
95fn stdlib_module_artifact(
96    module: &str,
97    synthetic: &Path,
98    source: &'static str,
99) -> Result<Arc<PreparedModuleArtifact>, VmError> {
100    let key = stdlib_artifact_cache_key(module, source);
101    {
102        let cache = stdlib_module_artifact_cache().lock().unwrap();
103        if let Some(cached) = cache.get(&key) {
104            return Ok(Arc::clone(cached));
105        }
106    }
107
108    // Stdlib modules are embedded in the binary so their content cannot
109    // legitimately change between processes; that means the disk cache
110    // for stdlib can use a synthetic source_path. The harn_version field
111    // of the cache key gates correctness across releases.
112    let lookup = bytecode_cache::load_module(synthetic, source);
113    let artifact = if let Some(artifact) = lookup.artifact {
114        artifact
115    } else {
116        let compiled = compile_module_artifact_from_source(synthetic, source)?;
117        if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
118            if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
119                eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
120            }
121        }
122        compiled
123    };
124
125    let compiled = Arc::new(PreparedModuleArtifact::from_cached(artifact));
126    let mut cache = stdlib_module_artifact_cache().lock().unwrap();
127    if let Some(cached) = cache.get(&key) {
128        return Ok(Arc::clone(cached));
129    }
130    cache.insert(key, Arc::clone(&compiled));
131    Ok(compiled)
132}
133
134impl Vm {
135    /// Resolve a callable against this VM. Lazy callables initialize once per
136    /// VM execution tree, then retain that module scope for later child VMs in
137    /// the same tree. Fresh VM roots remain isolated.
138    pub async fn resolve_callable(
139        &mut self,
140        callable: &crate::value::VmCallable,
141    ) -> Result<Arc<crate::value::VmClosure>, VmError> {
142        self.ensure_execution_available()?;
143        match callable {
144            crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
145            crate::value::VmCallable::Lazy(lazy) => {
146                let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
147                let resolution = {
148                    let mut modules = self.lazy_callable_modules.lock();
149                    Arc::clone(
150                        modules
151                            .entry(cache_key)
152                            .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new())),
153                    )
154                };
155                let resolved = resolution
156                    .get_or_try_init(|| async {
157                        let exports = self.load_module_exports(&module_path).await?;
158                        let exports = exports
159                            .into_iter()
160                            .map(|(name, closure)| (name, closure.retained_for_host_registry()))
161                            .collect();
162                        // Pin the complete module graph loaded above so that a
163                        // handler's transitively imported callees keep their
164                        // home-module registries/state alive for later child
165                        // VMs that hit this cache without re-importing.
166                        Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
167                            exports,
168                            retained_module_graph: Arc::clone(&self.module_cache),
169                        }))
170                    })
171                    .await?;
172                resolved
173                    .exports
174                    .get(&lazy.function_name)
175                    .cloned()
176                    .ok_or_else(|| {
177                        VmError::Runtime(format!(
178                            "function '{}' is not exported by module '{}'",
179                            lazy.function_name,
180                            lazy.module_path.display()
181                        ))
182                    })
183            }
184        }
185    }
186
187    fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
188        let mut module_path = if lazy.module_path.is_absolute() {
189            lazy.module_path.clone()
190        } else {
191            self.source_dir
192                .clone()
193                .unwrap_or_else(|| PathBuf::from("."))
194                .join(&lazy.module_path)
195        };
196        if !module_path.exists() && module_path.extension().is_none() {
197            module_path.set_extension("harn");
198        }
199        let cache_key = module_path
200            .canonicalize()
201            .unwrap_or_else(|_| module_path.clone());
202        (cache_key, module_path)
203    }
204
205    async fn load_module_from_source(
206        &mut self,
207        synthetic: PathBuf,
208        source: &str,
209    ) -> Result<LoadedModule, VmError> {
210        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
211            return Ok(loaded);
212        }
213        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
214
215        let artifact = PreparedModuleArtifact::from_cached(compile_module_artifact_from_source(
216            &synthetic, source,
217        )?);
218
219        self.imported_paths.push(synthetic.clone());
220        let loaded = self.instantiate_module(None, &artifact).await?;
221        self.imported_paths.pop();
222        Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
223        Ok(loaded)
224    }
225
226    async fn load_stdlib_module_from_source(
227        &mut self,
228        module: &str,
229        synthetic: PathBuf,
230        source: &'static str,
231    ) -> Result<LoadedModule, VmError> {
232        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
233            return Ok(loaded);
234        }
235        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
236
237        let artifact = stdlib_module_artifact(module, &synthetic, source)?;
238        self.imported_paths.push(synthetic.clone());
239        let loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
240        self.imported_paths.pop();
241        Arc::make_mut(&mut self.module_cache).insert(synthetic, loaded.clone());
242        Ok(loaded)
243    }
244
245    async fn instantiate_stdlib_module(
246        &mut self,
247        artifact: &PreparedModuleArtifact,
248    ) -> Result<LoadedModule, VmError> {
249        self.instantiate_module(None, artifact).await
250    }
251
252    /// Instantiate a previously-hydrated [`PreparedModuleArtifact`] into a
253    /// [`LoadedModule`]. Re-runs nested imports, replays the init chunk
254    /// into a fresh module env, mints a [`VmClosure`] for each compiled
255    /// function (stamped with `module_source_dir` so imports from inside
256    /// those functions resolve against the originating file), and
257    /// applies the re-export pass. Used by both stdlib and user-import
258    /// code paths.
259    async fn instantiate_module(
260        &mut self,
261        module_source_dir: Option<PathBuf>,
262        artifact: &PreparedModuleArtifact,
263    ) -> Result<LoadedModule, VmError> {
264        let caller_env = self.env.clone();
265        let old_source_dir = self.source_dir.clone();
266        self.env = VmEnv::new();
267        self.source_dir = module_source_dir.clone();
268
269        for import in &artifact.imports {
270            self.execute_import(&import.path, import.selected_names.as_deref())
271                .await?;
272        }
273
274        let module_state: crate::value::ModuleState = {
275            let mut init_env = self.env.clone();
276            if let Some(init_chunk) = &artifact.init_chunk {
277                let saved_env = std::mem::replace(&mut self.env, init_env);
278                let saved_frames = std::mem::take(&mut self.frames);
279                let saved_handlers = std::mem::take(&mut self.exception_handlers);
280                let saved_iterators = std::mem::take(&mut self.iterators);
281                let saved_deadlines = std::mem::take(&mut self.deadlines);
282                // STEP_STACK / PERSONA_STACK are thread-locals shared with
283                // the calling frame. Emptying `self.frames` above means
284                // any `prune_below_frame(0)` triggered while the init
285                // chunk's bytecode runs — including the inevitable
286                // frame-pop prune at end-of-chunk — would wipe active
287                // steps owned by the *caller* (e.g., a `@step`-decorated
288                // function whose body lazily imports a module). Snapshot
289                // the persona/step context here and restore it after init
290                // so module loading is invisible to the step-tracking
291                // surface.
292                let active_context = crate::step_runtime::take_active_context();
293                let init_result = self.run_chunk(Arc::clone(init_chunk)).await;
294                crate::step_runtime::restore_active_context(active_context);
295                init_env = std::mem::replace(&mut self.env, saved_env);
296                self.frames = saved_frames;
297                self.exception_handlers = saved_handlers;
298                self.iterators = saved_iterators;
299                self.deadlines = saved_deadlines;
300                init_result?;
301            }
302            Arc::new(crate::value::VmMutex::new(init_env))
303        };
304
305        let module_env = self.env.clone();
306        let registry: ModuleFunctionRegistry =
307            Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
308        let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
309        let mut public_names = artifact.public_names.clone();
310        // `pub const` / `pub let`: the init chunk already ran into `module_state`,
311        // so the bound values are live there. Read each exported value name out
312        // and publish it so importers can bind it. Add the names to
313        // `public_names` too, so the selective/wildcard export machinery treats
314        // them as part of the public surface (validation, re-export lists).
315        let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
316        {
317            let state = module_state.lock();
318            for name in &artifact.public_value_names {
319                if let Some(value) = state.get(name) {
320                    public_values.insert(name.clone(), value);
321                    public_names.insert(name.clone());
322                }
323            }
324        }
325        let mut public_type_names = artifact.public_type_names.clone();
326        let mut public_type_schemas: BTreeMap<String, VmValue> = artifact
327            .public_type_schemas
328            .iter()
329            .filter_map(|(name, json)| {
330                let parsed = serde_json::from_str::<serde_json::Value>(json).ok()?;
331                Some((name.clone(), crate::schema::json_to_vm_value(&parsed)))
332            })
333            .collect();
334
335        for (name, compiled) in &artifact.functions {
336            let closure = Arc::new(VmClosure {
337                func: Arc::clone(compiled),
338                env: module_env.clone(),
339                source_dir: module_source_dir.clone(),
340                module_functions: Some(Arc::downgrade(&registry)),
341                module_state: Some(Arc::downgrade(&module_state)),
342                retained_module_scope: None,
343            });
344            registry.lock().insert(name.clone(), Arc::clone(&closure));
345            self.env
346                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
347            module_state
348                .lock()
349                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
350            functions.insert(name.clone(), Arc::clone(&closure));
351        }
352
353        for import in artifact.imports.iter().filter(|import| import.is_pub) {
354            let cache_key = self.cache_key_for_import(&import.path);
355            let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
356                // A plain `import`/`import {...}` across a cycle is bound late
357                // by `flush_deferred_cyclic_imports`, but a `pub import`
358                // re-export has to publish the names into *this* module's
359                // public surface right now — and the target is still mid-load,
360                // so its surface does not exist yet. Name the cycle explicitly
361                // instead of the misleading "was not loaded".
362                if self.imported_paths.contains(&cache_key) {
363                    return Err(VmError::Runtime(format!(
364                        "Re-export error: cannot `pub import` from '{}' because it forms an \
365                         import cycle with this module (its public surface is still being \
366                         built). Use a plain `import` here, or re-export from a module that is \
367                         not part of the cycle.",
368                        import.path
369                    )));
370                }
371                return Err(VmError::Runtime(format!(
372                    "Re-export error: imported module '{}' was not loaded",
373                    import.path
374                )));
375            };
376            let names_to_reexport: Vec<String> = match &import.selected_names {
377                Some(names) => names.clone(),
378                // A wildcard `pub import` re-exports exactly the target's `pub`
379                // surface (functions and erased `pub type` aliases). A module
380                // with no `pub` declarations exports nothing.
381                None => loaded
382                    .public_names
383                    .iter()
384                    .chain(loaded.public_type_names.iter())
385                    .cloned()
386                    .collect(),
387            };
388            for name in names_to_reexport {
389                let Some(closure) = loaded.functions.get(&name) else {
390                    // `pub const` / `pub let` values carry no closure: re-export
391                    // the value directly.
392                    if let Some(value) = loaded.public_values.get(&name) {
393                        public_values.insert(name.clone(), value.clone());
394                        public_names.insert(name);
395                        continue;
396                    }
397                    // `pub type` aliases are erased at runtime: re-export the
398                    // name (and its schema lowering, when present) for
399                    // importers, with no closure to bind.
400                    if loaded.public_type_names.contains(&name) {
401                        if let Some(schema) = loaded.public_type_schemas.get(&name) {
402                            public_type_schemas.insert(name.clone(), schema.clone());
403                        }
404                        public_type_names.insert(name);
405                        continue;
406                    }
407                    return Err(VmError::Runtime(format!(
408                        "Re-export error: '{name}' is not exported by '{}'",
409                        import.path
410                    )));
411                };
412                if let Some(existing) = functions.get(&name) {
413                    if !Arc::ptr_eq(existing, closure) {
414                        return Err(VmError::Runtime(format!(
415                            "Re-export collision: '{name}' is defined here and also \
416                             re-exported from '{}'",
417                            import.path
418                        )));
419                    }
420                }
421                functions.insert(name.clone(), Arc::clone(closure));
422                public_names.insert(name);
423            }
424        }
425
426        self.env = caller_env;
427        self.source_dir = old_source_dir;
428
429        Ok(LoadedModule {
430            functions,
431            public_names,
432            public_values,
433            public_type_names,
434            public_type_schemas,
435            _module_functions: registry,
436            _module_state: module_state,
437        })
438    }
439
440    fn export_loaded_module(
441        &mut self,
442        module_path: &Path,
443        loaded: &LoadedModule,
444        selected_names: Option<&[String]>,
445    ) -> Result<(), VmError> {
446        let module_name = module_path.display().to_string();
447        let export_names: Vec<String> = if let Some(names) = selected_names {
448            // Selective imports may only name symbols the module marks `pub`.
449            // A module with no `pub` functions exports nothing — matching every
450            // strict-visibility language (TypeScript, Rust, Go) and removing
451            // the old footgun where adding the first `pub` silently turned every
452            // other (previously importable) function private to callers.
453            for name in names {
454                if !loaded.public_names.contains(name) && !loaded.public_type_names.contains(name) {
455                    let hint = if loaded.functions.contains_key(name) {
456                        " — it is defined there but not `pub`; mark it `pub` to export it"
457                    } else {
458                        ""
459                    };
460                    return Err(VmError::Runtime(format!(
461                        "Import error: '{name}' is not exported by {module_name}{hint}"
462                    )));
463                }
464            }
465            names.to_vec()
466        } else {
467            // Wildcard import brings in exactly the module's `pub` surface,
468            // including erased `pub type` aliases.
469            loaded
470                .public_names
471                .iter()
472                .chain(loaded.public_type_names.iter())
473                .cloned()
474                .collect()
475        };
476
477        for name in export_names {
478            // `pub type` aliases are erased at runtime: the import is valid
479            // (the type checker consumed it). When the alias lowers to a JSON
480            // schema, bind the name to that dict so expression-position uses
481            // (`output_schema: ImportedAlias`, `schema_is(x, ImportedAlias)`)
482            // behave like a locally declared alias; otherwise bind nothing.
483            if loaded.public_type_names.contains(&name) && !loaded.functions.contains_key(&name) {
484                if let Some(schema) = loaded.public_type_schemas.get(&name) {
485                    self.env.define(&name, schema.clone(), false)?;
486                }
487                continue;
488            }
489            // `pub const` / `pub let` values: bind by value.
490            if let Some(value) = loaded.public_values.get(&name) {
491                if self.env.get(&name).is_some() {
492                    return Err(VmError::Runtime(format!(
493                        "Import collision: '{name}' is already defined when importing \
494                         {module_name}. Use selective imports to disambiguate: \
495                         import {{ {name} }} from \"...\""
496                    )));
497                }
498                self.env.define(&name, value.clone(), false)?;
499                continue;
500            }
501            let Some(closure) = loaded.functions.get(&name) else {
502                return Err(VmError::Runtime(format!(
503                    "Import error: '{name}' is not defined in {module_name}"
504                )));
505            };
506            if let Some(VmValue::Closure(_)) = self.env.get(&name) {
507                return Err(VmError::Runtime(format!(
508                    "Import collision: '{name}' is already defined when importing {module_name}. \
509                     Use selective imports to disambiguate: import {{ {name} }} from \"...\""
510                )));
511            }
512            self.env
513                .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
514        }
515        Ok(())
516    }
517
518    /// Execute an import, reading and running the file's declarations.
519    pub(super) fn execute_import<'a>(
520        &'a mut self,
521        path: &'a str,
522        selected_names: Option<&'a [String]>,
523    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
524        Box::pin(async move {
525            let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
526
527            let stdlib_module = path
528                .strip_prefix("std/")
529                .or_else(|| (path == "observability").then_some("observability"));
530            if let Some(module) = stdlib_module {
531                if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
532                    let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
533                    if self.imported_paths.contains(&synthetic) {
534                        return Ok(());
535                    }
536                    let loaded = self
537                        .load_stdlib_module_from_source(module, synthetic.clone(), source)
538                        .await?;
539                    self.export_loaded_module(&synthetic, &loaded, selected_names)?;
540                    return Ok(());
541                }
542                return Err(VmError::Runtime(format!(
543                    "Unknown stdlib module: std/{module}"
544                )));
545            }
546
547            let base = self
548                .source_dir
549                .clone()
550                .unwrap_or_else(|| PathBuf::from("."));
551            let file_path = resolve_module_import_path(&base, path);
552
553            let canonical = file_path
554                .canonicalize()
555                .unwrap_or_else(|_| file_path.clone());
556            if self.imported_paths.contains(&canonical) {
557                // Import cycle: `canonical` is still mid-load (it sits on the
558                // import stack), so its function closures don't exist yet and
559                // we cannot bind the requested names inline. Record the import
560                // and resolve it once both modules finish loading — otherwise
561                // whichever module happens to close the cycle silently loses
562                // these bindings and fails with `Undefined builtin` at call
563                // time, in a load-order-dependent way.
564                if let Some(importer) = self.imported_paths.last().cloned() {
565                    if importer != canonical {
566                        self.deferred_cyclic_imports.push(DeferredCyclicImport {
567                            importer,
568                            target: canonical.clone(),
569                            selected_names: selected_names.map(<[String]>::to_vec),
570                        });
571                    }
572                }
573                return Ok(());
574            }
575            if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
576                return self.export_loaded_module(&canonical, &loaded, selected_names);
577            }
578            self.imported_paths.push(canonical.clone());
579
580            let source = std::fs::read_to_string(&file_path).map_err(|e| {
581                // Name the resolution base: relative imports resolve against the
582                // importing file's dir (or CWD when unset), so an error that
583                // prints only the joined path leaves the author guessing which
584                // base was used.
585                VmError::Runtime(format!(
586                    "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
587                    file_path.display(),
588                    base.display()
589                ))
590            })?;
591            Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
592            Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
593
594            let prepared = if bytecode_cache::cache_enabled() {
595                self.prepared_module_cache.get(&canonical, &source)
596            } else {
597                None
598            };
599            let artifact = if let Some(prepared) = prepared {
600                prepared
601            } else {
602                // Disk cache hits skip parse + compile. The scoped prepared
603                // cache additionally skips deserialization and chunk hydration
604                // on later fresh VMs without sharing any runtime module state.
605                let lookup = bytecode_cache::load_module(&file_path, &source);
606                let cached = if let Some(artifact) = lookup.artifact {
607                    artifact
608                } else {
609                    let compiled = compile_module_artifact_from_source(&file_path, &source)?;
610                    if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
611                        if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
612                            eprintln!(
613                                "[harn] module cache write skipped for {}: {err}",
614                                file_path.display()
615                            );
616                        }
617                    }
618                    compiled
619                };
620                let mut prepared = Arc::new(PreparedModuleArtifact::from_cached(cached));
621                if bytecode_cache::cache_enabled() {
622                    prepared =
623                        self.prepared_module_cache
624                            .insert(canonical.clone(), &source, prepared);
625                }
626                prepared
627            };
628
629            let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
630            let loaded = self
631                .instantiate_module(module_source_dir, artifact.as_ref())
632                .await?;
633            self.imported_paths.pop();
634            Arc::make_mut(&mut self.module_cache).insert(canonical.clone(), loaded.clone());
635            self.export_loaded_module(&canonical, &loaded, selected_names)?;
636
637            // Once the import stack fully unwinds, every module reachable from
638            // this top-level import is cached, so any deferred cyclic imports
639            // can now bind against fully-loaded modules.
640            if self.imported_paths.is_empty() {
641                self.flush_deferred_cyclic_imports()?;
642            }
643
644            Ok(())
645        })
646    }
647
648    /// Bind imports that were deferred because their target module was still
649    /// mid-load (an import cycle). By the time the import stack has unwound,
650    /// both the importing and target modules are fully instantiated and cached,
651    /// so we can resolve the requested names against the target and define them
652    /// into the importer's shared, mutable `module_state`. That env is the one
653    /// every closure from the importing module consults (after its local env)
654    /// at call time, so the late binding becomes visible without needing to
655    /// rewrite the closures' captured lexical snapshots.
656    fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
657        if self.deferred_cyclic_imports.is_empty() {
658            return Ok(());
659        }
660        let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
661        let mut still_pending = Vec::new();
662        for import in deferred {
663            let (Some(importer), Some(target)) = (
664                self.module_cache.get(&import.importer).cloned(),
665                self.module_cache.get(&import.target).cloned(),
666            ) else {
667                // One endpoint is not cached yet (a lazy import inside a
668                // function body can defer before the other side loads). Keep
669                // it for a later flush.
670                still_pending.push(import);
671                continue;
672            };
673
674            let export_names: Vec<String> = match &import.selected_names {
675                Some(names) => names.clone(),
676                None if !target.public_names.is_empty() => {
677                    target.public_names.iter().cloned().collect()
678                }
679                None => target.functions.keys().cloned().collect(),
680            };
681
682            let mut module_state = importer._module_state.lock();
683            for name in export_names {
684                // A real local declaration (or an already-bound non-cyclic
685                // import) wins over the cyclic re-binding.
686                if module_state.get(&name).is_some() {
687                    continue;
688                }
689                if let Some(closure) = target.functions.get(&name) {
690                    module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
691                } else if let Some(value) = target.public_values.get(&name) {
692                    // `pub const` / `pub let` imported across a cycle.
693                    module_state.define(&name, value.clone(), false)?;
694                } else {
695                    return Err(VmError::Runtime(format!(
696                        "Import error: '{name}' is not defined in {}",
697                        import.target.display()
698                    )));
699                }
700            }
701        }
702        self.deferred_cyclic_imports = still_pending;
703        Ok(())
704    }
705
706    /// Return the path key that `execute_import` would use to cache the
707    /// LoadedModule for this import string. Used by the re-export pass to
708    /// look up the already-loaded source module after `execute_import`
709    /// has populated [`Vm::module_cache`].
710    fn cache_key_for_import(&self, path: &str) -> PathBuf {
711        if let Some(module) = path
712            .strip_prefix("std/")
713            .or_else(|| (path == "observability").then_some("observability"))
714        {
715            return PathBuf::from(format!("<stdlib>/{module}.harn"));
716        }
717        let base = self
718            .source_dir
719            .clone()
720            .unwrap_or_else(|| PathBuf::from("."));
721        let file_path = resolve_module_import_path(&base, path);
722        file_path.canonicalize().unwrap_or(file_path)
723    }
724
725    /// Load a module file and return the exported function closures that
726    /// would be visible to a wildcard import.
727    pub async fn load_module_exports(
728        &mut self,
729        path: &Path,
730    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
731        self.ensure_execution_available()?;
732        let path_str = path.to_string_lossy().into_owned();
733        self.execute_import(&path_str, None).await?;
734
735        let mut file_path = if path.is_absolute() {
736            path.to_path_buf()
737        } else {
738            self.source_dir
739                .clone()
740                .unwrap_or_else(|| PathBuf::from("."))
741                .join(path)
742        };
743        if !file_path.exists() && file_path.extension().is_none() {
744            file_path.set_extension("harn");
745        }
746
747        let canonical = file_path
748            .canonicalize()
749            .unwrap_or_else(|_| file_path.clone());
750        let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
751            VmError::Runtime(format!(
752                "Import error: failed to cache loaded module '{}'",
753                canonical.display()
754            ))
755        })?;
756
757        let export_names: Vec<String> = if loaded.public_names.is_empty() {
758            loaded.functions.keys().cloned().collect()
759        } else {
760            loaded.public_names.iter().cloned().collect()
761        };
762
763        let mut exports = BTreeMap::new();
764        for name in export_names {
765            let Some(closure) = loaded.functions.get(&name) else {
766                return Err(VmError::Runtime(format!(
767                    "Import error: exported function '{name}' is missing from {}",
768                    canonical.display()
769                )));
770            };
771            exports.insert(name, Arc::clone(closure));
772        }
773
774        Ok(exports)
775    }
776
777    /// Load synthetic source keyed by a synthetic module path and return
778    /// the exported function closures that a wildcard import would expose.
779    pub async fn load_module_exports_from_source(
780        &mut self,
781        source_key: impl Into<PathBuf>,
782        source: &str,
783    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
784        self.ensure_execution_available()?;
785        let synthetic = source_key.into();
786        let loaded = self
787            .load_module_from_source(synthetic.clone(), source)
788            .await?;
789        let export_names: Vec<String> = if loaded.public_names.is_empty() {
790            loaded.functions.keys().cloned().collect()
791        } else {
792            loaded.public_names.iter().cloned().collect()
793        };
794
795        let mut exports = BTreeMap::new();
796        for name in export_names {
797            let Some(closure) = loaded.functions.get(&name) else {
798                return Err(VmError::Runtime(format!(
799                    "Import error: exported function '{name}' is missing from {}",
800                    synthetic.display()
801                )));
802            };
803            exports.insert(name, Arc::clone(closure));
804        }
805
806        Ok(exports)
807    }
808
809    /// Load a module by import path (`std/foo`, relative module path, or
810    /// package import) and return the exported function closures that a
811    /// wildcard import would expose.
812    pub async fn load_module_exports_from_import(
813        &mut self,
814        import_path: &str,
815    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
816        self.ensure_execution_available()?;
817        self.execute_import(import_path, None).await?;
818
819        if let Some(module) = import_path
820            .strip_prefix("std/")
821            .or_else(|| (import_path == "observability").then_some("observability"))
822        {
823            let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
824            let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
825                VmError::Runtime(format!(
826                    "Import error: failed to cache loaded module '{}'",
827                    synthetic.display()
828                ))
829            })?;
830            let mut exports = BTreeMap::new();
831            let export_names: Vec<String> = if loaded.public_names.is_empty() {
832                loaded.functions.keys().cloned().collect()
833            } else {
834                loaded.public_names.iter().cloned().collect()
835            };
836            for name in export_names {
837                let Some(closure) = loaded.functions.get(&name) else {
838                    return Err(VmError::Runtime(format!(
839                        "Import error: exported function '{name}' is missing from {}",
840                        synthetic.display()
841                    )));
842                };
843                exports.insert(name, Arc::clone(closure));
844            }
845            return Ok(exports);
846        }
847
848        let base = self
849            .source_dir
850            .clone()
851            .unwrap_or_else(|| PathBuf::from("."));
852        let file_path = resolve_module_import_path(&base, import_path);
853        self.load_module_exports(&file_path).await
854    }
855}
856
857#[cfg(test)]
858mod tests {
859
860    use std::sync::{Mutex, MutexGuard, OnceLock};
861
862    use super::*;
863
864    static CACHE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
865
866    fn cache_test_guard() -> MutexGuard<'static, ()> {
867        CACHE_TEST_LOCK
868            .get_or_init(|| Mutex::new(()))
869            .lock()
870            .unwrap()
871    }
872
873    fn cached_stdlib_module_ptr(module: &str) -> Option<usize> {
874        let source = harn_stdlib::get_stdlib_source(module).expect("stdlib module source exists");
875        stdlib_module_artifact_cache_ptr(module, source)
876    }
877
878    #[test]
879    fn module_function_can_use_local_type_alias_as_schema_value() {
880        let runtime = tokio::runtime::Builder::new_current_thread()
881            .enable_all()
882            .build()
883            .expect("runtime builds");
884
885        let result = runtime.block_on(async {
886            let mut vm = Vm::new();
887            crate::stdlib::register_vm_stdlib(&mut vm);
888            let loaded = vm
889                .load_module_from_source(
890                    PathBuf::from("<test>/schema_alias_module.harn"),
891                    r#"
892fn accepts_schema(schema) {
893  return schema_report({name: "Ada"}, schema).ok
894}
895
896type UserShape = {name: string}
897
898pub fn works() {
899  return accepts_schema(UserShape)
900}
901"#,
902                )
903                .await
904                .expect("module loads");
905            let closure = Arc::clone(loaded.functions.get("works").expect("works export exists"));
906            vm.call_closure_pub(&closure, &[])
907                .await
908                .expect("module closure executes")
909        });
910
911        assert!(matches!(result, VmValue::Bool(true)), "{result:?}");
912    }
913
914    #[test]
915    fn stdlib_artifact_cache_reuses_compilation_with_fresh_vm_state() {
916        let _guard = cache_test_guard();
917        reset_stdlib_module_artifact_cache();
918        let runtime = tokio::runtime::Builder::new_current_thread()
919            .enable_all()
920            .build()
921            .expect("runtime builds");
922
923        let (first_exports, second_exports, first_state_weak, second_state_weak) = runtime
924            .block_on(async {
925                let mut first_vm = Vm::new();
926                let first_exports = first_vm
927                    .load_module_exports_from_import("std/agent/prompts")
928                    .await
929                    .expect("first stdlib import succeeds");
930                let first_state = first_exports
931                    .get("render_agent_prompt")
932                    .expect("first export exists")
933                    .module_state()
934                    .expect("first module state stays live while VM owns module");
935                let first_state_weak = Arc::downgrade(&first_state);
936                let first_state_ptr = Arc::as_ptr(&first_state);
937
938                let mut second_vm = Vm::new();
939                let second_exports = second_vm
940                    .load_module_exports_from_import("std/agent/prompts")
941                    .await
942                    .expect("second stdlib import succeeds");
943                let second_state = second_exports
944                    .get("render_agent_prompt")
945                    .expect("second export exists")
946                    .module_state()
947                    .expect("second module state stays live while VM owns module");
948                let second_state_weak = Arc::downgrade(&second_state);
949
950                assert_ne!(first_state_ptr, Arc::as_ptr(&second_state));
951                (
952                    first_exports,
953                    second_exports,
954                    first_state_weak,
955                    second_state_weak,
956                )
957            });
958        let first_cached =
959            cached_stdlib_module_ptr("agent/prompts").expect("first import cached stdlib artifact");
960        assert_eq!(
961            cached_stdlib_module_ptr("agent/prompts"),
962            Some(first_cached)
963        );
964
965        let first = first_exports
966            .get("render_agent_prompt")
967            .expect("first export exists");
968        let second = second_exports
969            .get("render_agent_prompt")
970            .expect("second export exists");
971
972        assert!(!Arc::ptr_eq(first, second));
973        assert!(Arc::ptr_eq(&first.func, &second.func));
974        assert!(Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
975        assert!(first.module_state().is_none());
976        assert!(second.module_state().is_none());
977        assert!(first_state_weak.upgrade().is_none());
978        assert!(second_state_weak.upgrade().is_none());
979    }
980
981    #[test]
982    fn prepared_user_module_reuses_code_with_fresh_mutable_state() {
983        let runtime = tokio::runtime::Builder::new_current_thread()
984            .enable_all()
985            .build()
986            .expect("runtime builds");
987        let temp = tempfile::tempdir().expect("tempdir");
988        let module = temp.path().join("counter.harn");
989        std::fs::write(
990            &module,
991            r"
992let count = 0
993
994pub fn increment() {
995  count = count + 1
996  return count
997}
998",
999        )
1000        .expect("write module");
1001        let cache = crate::PreparedModuleCache::default();
1002
1003        runtime.block_on(async {
1004            let mut first_vm = Vm::new();
1005            first_vm.set_prepared_module_cache(cache.clone());
1006            let first_exports = first_vm
1007                .load_module_exports(&module)
1008                .await
1009                .expect("first module load succeeds");
1010            let first = first_exports.get("increment").expect("first export");
1011            assert!(matches!(
1012                first_vm.call_closure_pub(first, &[]).await,
1013                Ok(VmValue::Int(1))
1014            ));
1015            assert!(matches!(
1016                first_vm.call_closure_pub(first, &[]).await,
1017                Ok(VmValue::Int(2))
1018            ));
1019
1020            let mut second_vm = Vm::new();
1021            second_vm.set_prepared_module_cache(cache.clone());
1022            let second_exports = second_vm
1023                .load_module_exports(&module)
1024                .await
1025                .expect("second module load succeeds");
1026            let second = second_exports.get("increment").expect("second export");
1027
1028            assert!(!Arc::ptr_eq(first, second));
1029            assert!(Arc::ptr_eq(&first.func, &second.func));
1030            assert!(Arc::ptr_eq(&first.func.chunk, &second.func.chunk));
1031            assert_ne!(
1032                Arc::as_ptr(&first.module_state().expect("first state")),
1033                Arc::as_ptr(&second.module_state().expect("second state"))
1034            );
1035            assert!(matches!(
1036                second_vm.call_closure_pub(second, &[]).await,
1037                Ok(VmValue::Int(1))
1038            ));
1039        });
1040
1041        let stats = cache.stats();
1042        assert_eq!(stats.insertions, 1);
1043        assert_eq!(stats.hits, 1);
1044        assert_eq!(stats.entries, 1);
1045    }
1046
1047    #[test]
1048    fn prepared_importer_reloads_changed_dependency() {
1049        let runtime = tokio::runtime::Builder::new_current_thread()
1050            .enable_all()
1051            .build()
1052            .expect("runtime builds");
1053        let temp = tempfile::tempdir().expect("tempdir");
1054        let module = temp.path().join("reader.harn");
1055        let dependency = temp.path().join("value.harn");
1056        std::fs::write(
1057            &module,
1058            "import { value } from \"./value\"\npub fn read() { return value() }\n",
1059        )
1060        .expect("write importer");
1061        std::fs::write(&dependency, "pub fn value() { return 1 }\n").expect("write dependency");
1062        let cache = crate::PreparedModuleCache::default();
1063
1064        runtime.block_on(async {
1065            let mut first_vm = Vm::new();
1066            first_vm.set_prepared_module_cache(cache.clone());
1067            let first_exports = first_vm
1068                .load_module_exports(&module)
1069                .await
1070                .expect("first module load succeeds");
1071            let first = first_exports.get("read").expect("first export");
1072            let first_result = first_vm.call_closure_pub(first, &[]).await;
1073            assert!(
1074                matches!(first_result, Ok(VmValue::Int(1))),
1075                "unexpected first dependency result: {first_result:?}"
1076            );
1077
1078            std::fs::write(&dependency, "pub fn value() { return 2 }\n")
1079                .expect("rewrite dependency");
1080
1081            let mut second_vm = Vm::new();
1082            second_vm.set_prepared_module_cache(cache.clone());
1083            let second_exports = second_vm
1084                .load_module_exports(&module)
1085                .await
1086                .expect("second module load succeeds");
1087            let second = second_exports.get("read").expect("second export");
1088            let second_result = second_vm.call_closure_pub(second, &[]).await;
1089            assert!(
1090                matches!(second_result, Ok(VmValue::Int(2))),
1091                "unexpected refreshed dependency result: {second_result:?}"
1092            );
1093        });
1094
1095        let stats = cache.stats();
1096        assert_eq!(stats.hits, 1);
1097        assert_eq!(stats.insertions, 3);
1098        assert_eq!(stats.entries, 3);
1099    }
1100
1101    #[test]
1102    fn stdlib_artifact_cache_is_process_wide_across_threads() {
1103        let _guard = cache_test_guard();
1104        reset_stdlib_module_artifact_cache();
1105
1106        let handle = std::thread::spawn(|| {
1107            let runtime = tokio::runtime::Builder::new_current_thread()
1108                .enable_all()
1109                .build()
1110                .expect("runtime builds");
1111            runtime.block_on(async {
1112                let mut vm = Vm::new();
1113                vm.load_module_exports_from_import("std/agent/prompts")
1114                    .await
1115                    .expect("thread stdlib import succeeds");
1116            });
1117        });
1118        handle.join().expect("thread joins");
1119        let thread_cached = cached_stdlib_module_ptr("agent/prompts")
1120            .expect("thread import cached stdlib artifact");
1121
1122        let runtime = tokio::runtime::Builder::new_current_thread()
1123            .enable_all()
1124            .build()
1125            .expect("runtime builds");
1126        runtime.block_on(async {
1127            let mut vm = Vm::new();
1128            vm.load_module_exports_from_import("std/agent/prompts")
1129                .await
1130                .expect("main-thread stdlib import succeeds");
1131        });
1132        assert_eq!(
1133            cached_stdlib_module_ptr("agent/prompts"),
1134            Some(thread_cached)
1135        );
1136    }
1137
1138    #[test]
1139    fn module_closures_release_state_after_vm_drop() {
1140        let runtime = tokio::runtime::Builder::new_current_thread()
1141            .enable_all()
1142            .build()
1143            .expect("runtime builds");
1144
1145        let (closure_weak, registry_weak, state_weak) = runtime.block_on(async {
1146            let mut vm = Vm::new();
1147            let loaded = vm
1148                .load_module_from_source(
1149                    PathBuf::from("<test>/module_cycle.harn"),
1150                    r#"
1151let payload = "x" * 1024
1152
1153pub fn touch() {
1154  return len(payload)
1155}
1156"#,
1157                )
1158                .await
1159                .expect("module loads");
1160            let closure = Arc::clone(loaded.functions.get("touch").expect("touch export exists"));
1161            let closure_weak = Arc::downgrade(&closure);
1162            let registry_weak = Arc::downgrade(&loaded._module_functions);
1163            let state_weak = Arc::downgrade(&loaded._module_state);
1164
1165            drop(closure);
1166            drop(loaded);
1167            drop(vm);
1168
1169            (closure_weak, registry_weak, state_weak)
1170        });
1171
1172        assert!(
1173            closure_weak.upgrade().is_none(),
1174            "module closure should drop with its VM"
1175        );
1176        assert!(
1177            registry_weak.upgrade().is_none(),
1178            "module function registry should drop with its VM"
1179        );
1180        assert!(
1181            state_weak.upgrade().is_none(),
1182            "module state should drop with its VM"
1183        );
1184    }
1185}