Skip to main content

harn_vm/vm/
modules.rs

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