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
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    /// Evaluated values of exported `pub const` / `pub let` bindings, read out
51    /// of the instantiated module env after the init chunk ran. Importers bind
52    /// these by value (like every other cross-module value). Disjoint from
53    /// `functions` and `public_type_names`.
54    pub(crate) public_values: BTreeMap<String, VmValue>,
55    /// Names of `pub type` aliases (and re-exported ones). Erased at runtime:
56    /// selective imports may name them, but they bind no value of their own.
57    pub(crate) public_type_names: HashSet<String>,
58    /// Decoded JSON-Schema dict for each `pub type` alias that lowers to a
59    /// schema. Importers bind the alias name to this value so
60    /// expression-position uses (`output_schema: ImportedAlias`) work.
61    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
66/// Runtime module cache shared by child VMs within one execution tree.
67///
68/// The map stays copy-on-write so a child can add modules without mutating its
69/// parent. Cache entries are never replaced after instantiation, so cache hits
70/// and map copies share their export maps plus their existing shared
71/// registries/state through a cheap outer [`Arc`] instead of cloning the whole
72/// module.
73pub(crate) type ModuleCache = Arc<BTreeMap<PathBuf, Arc<LoadedModule>>>;
74
75/// An import whose target module was still mid-load (an import cycle) when the
76/// importing module reached it. The target's function closures don't exist yet
77/// at that point, so the binding can't happen inline. We record it here and
78/// resolve it once both modules are fully loaded — see
79/// [`Vm::flush_deferred_cyclic_imports`].
80#[derive(Clone, Debug)]
81pub(crate) struct DeferredCyclicImport {
82    /// Canonical path of the module that issued the import.
83    pub(crate) importer: PathBuf,
84    /// Canonical path of the cyclically-imported target module.
85    pub(crate) target: PathBuf,
86    /// Selectively-imported names, or `None` for a wildcard/side-effect import.
87    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    // Stdlib modules are embedded in the binary so their content cannot
127    // legitimately change between processes; that means the disk cache
128    // for stdlib can use a synthetic source_path. The harn_version field
129    // of the cache key gates correctness across releases.
130    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    /// Resolve a callable against this VM. Lazy callables initialize once per
185    /// VM execution tree, then retain that module scope for later child VMs in
186    /// the same tree. Fresh VM roots remain isolated.
187    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                        // Pin the complete module graph loaded above so that a
212                        // handler's transitively imported callees keep their
213                        // home-module registries/state alive for later child
214                        // VMs that hit this cache without re-importing.
215                        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    /// Instantiate a previously-hydrated [`PreparedModuleArtifact`] into a
403    /// [`LoadedModule`]. Re-runs nested imports, replays the init chunk
404    /// into a fresh module env, mints a [`VmClosure`] for each compiled
405    /// function (stamped with `module_source_dir` so imports from inside
406    /// those functions resolve against the originating file), and
407    /// applies the re-export pass. Used by both stdlib and user-import
408    /// code paths.
409    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        // Nested modules own their own load spans. Start this module's span
425        // only after those imports finish so aggregate load time is additive.
426        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                // STEP_STACK / PERSONA_STACK are thread-locals shared with
437                // the calling frame. Emptying `self.frames` above means
438                // any `prune_below_frame(0)` triggered while the init
439                // chunk's bytecode runs — including the inevitable
440                // frame-pop prune at end-of-chunk — would wipe active
441                // steps owned by the *caller* (e.g., a `@step`-decorated
442                // function whose body lazily imports a module). Snapshot
443                // the persona/step context here and restore it after init
444                // so module loading is invisible to the step-tracking
445                // surface.
446                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        // `pub const` / `pub let`: the init chunk already ran into `module_state`,
465        // so the bound values are live there. Read each exported value name out
466        // and publish it so importers can bind it. Add the names to
467        // `public_names` too, so the selective/wildcard export machinery treats
468        // them as part of the public surface (validation, re-export lists).
469        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(&registry)),
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                // A plain `import`/`import {...}` across a cycle is bound late
511                // by `flush_deferred_cyclic_imports`, but a `pub import`
512                // re-export has to publish the names into *this* module's
513                // public surface right now — and the target is still mid-load,
514                // so its surface does not exist yet. Name the cycle explicitly
515                // instead of the misleading "was not loaded".
516                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                // A wildcard `pub import` re-exports exactly the target's `pub`
533                // surface (functions and erased `pub type` aliases). A module
534                // with no `pub` declarations exports nothing.
535                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                    // `pub const` / `pub let` values carry no closure: re-export
545                    // the value directly.
546                    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                    // `pub type` aliases are erased at runtime: re-export the
552                    // name (and its schema lowering, when present) for
553                    // importers, with no closure to bind.
554                    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            // Selective imports may only name symbols the module marks `pub`.
603            // A module with no `pub` functions exports nothing — matching every
604            // strict-visibility language (TypeScript, Rust, Go) and removing
605            // the old footgun where adding the first `pub` silently turned every
606            // other (previously importable) function private to callers.
607            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            // Wildcard import brings in exactly the module's `pub` surface,
622            // including erased `pub type` aliases.
623            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            // `pub type` aliases are erased at runtime: the import is valid
633            // (the type checker consumed it). When the alias lowers to a JSON
634            // schema, bind the name to that dict so expression-position uses
635            // (`output_schema: ImportedAlias`, `schema_is(x, ImportedAlias)`)
636            // behave like a locally declared alias; otherwise bind nothing.
637            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            // `pub const` / `pub let` values: bind by value.
644            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    /// Execute an import, reading and running the file's declarations.
673    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                // Import cycle: `canonical` is still mid-load (it sits on the
726                // import stack), so its function closures don't exist yet and
727                // we cannot bind the requested names inline. Record the import
728                // and resolve it once both modules finish loading — otherwise
729                // whichever module happens to close the cycle silently loses
730                // these bindings and fails with `Undefined builtin` at call
731                // time, in a load-order-dependent way.
732                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                        // Name the resolution base: relative imports resolve against the
763                        // importing file's dir (or CWD when unset), so an error that
764                        // prints only the joined path leaves the author guessing which
765                        // base was used.
766                        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                // Disk cache hits skip parse + compile. The scoped prepared
789                // cache additionally skips deserialization and chunk hydration
790                // on later fresh VMs without sharing any runtime module state.
791                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            // Once the import stack fully unwinds, every module reachable from
844            // this top-level import is cached, so any deferred cyclic imports
845            // can now bind against fully-loaded modules.
846            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    /// Bind imports that were deferred because their target module was still
856    /// mid-load (an import cycle). By the time the import stack has unwound,
857    /// both the importing and target modules are fully instantiated and cached,
858    /// so we can resolve the requested names against the target and define them
859    /// into the importer's shared, mutable `module_state`. That env is the one
860    /// every closure from the importing module consults (after its local env)
861    /// at call time, so the late binding becomes visible without needing to
862    /// rewrite the closures' captured lexical snapshots.
863    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                // One endpoint is not cached yet (a lazy import inside a
875                // function body can defer before the other side loads). Keep
876                // it for a later flush.
877                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                // A real local declaration (or an already-bound non-cyclic
892                // import) wins over the cyclic re-binding.
893                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                    // `pub const` / `pub let` imported across a cycle.
900                    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    /// Return the path key that `execute_import` would use to cache the
914    /// LoadedModule for this import string. Used by the re-export pass to
915    /// look up the already-loaded source module after `execute_import`
916    /// has populated [`Vm::module_cache`].
917    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    /// Load a module file and return the exported function closures that
933    /// would be visible to a wildcard import.
934    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    /// Load synthetic source keyed by a synthetic module path and return
985    /// the exported function closures that a wildcard import would expose.
986    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    /// Load a module by import path (`std/foo`, relative module path, or
1017    /// package import) and return the exported function closures that a
1018    /// wildcard import would expose.
1019    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;