Skip to main content

harn_vm/vm/
modules.rs

1use std::collections::BTreeMap;
2use std::future::Future;
3use std::path::{Path, PathBuf};
4use std::pin::Pin;
5use std::sync::Arc;
6
7use harn_modules::DefKind;
8
9use crate::bytecode_cache;
10use crate::module_artifact::{
11    compile_embedded_stdlib_module_artifact_from_source, compile_module_artifact_from_source,
12    compile_trusted_host_dispatch_module_artifact_from_source, ModuleImportBinding,
13    ModuleProvenance,
14};
15use crate::module_source::{self, ModuleSource};
16use crate::prepared_module::PreparedModuleArtifact;
17use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
18
19use super::stdlib_artifact::stdlib_module_artifact;
20use super::{ScopeSpan, Vm};
21// Reached by `modules_tests` through this module's bindings.
22#[cfg(test)]
23use super::stdlib_artifact::{
24    reset_stdlib_module_artifact_cache, stdlib_artifact_get_or_prepare,
25    stdlib_module_artifact_cache_ptr,
26};
27
28fn verified_package_source(bytes: Vec<u8>, path: &Path) -> Result<String, VmError> {
29    String::from_utf8(bytes).map_err(|error| {
30        VmError::Runtime(format!(
31            "installed package source {} is not valid UTF-8: {error}",
32            path.display()
33        ))
34    })
35}
36
37fn exported_function_closures(
38    loaded: &LoadedModule,
39    display_path: &Path,
40) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
41    let mut exports = BTreeMap::new();
42    for name in loaded
43        .public_exports
44        .keys()
45        .filter(|name| loaded.functions.contains_key(*name))
46    {
47        let Some(closure) = loaded.functions.get(name) else {
48            return Err(VmError::Runtime(format!(
49                "Import error: exported function '{name}' is missing from {}",
50                display_path.display()
51            )));
52        };
53        exports.insert(name.clone(), Arc::clone(closure));
54    }
55    Ok(exports)
56}
57
58pub(crate) struct LoadedModule {
59    pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
60    /// Shared public declaration contract copied from the artifact and
61    /// extended by explicit re-exports.
62    pub(crate) public_exports: BTreeMap<String, DefKind>,
63    /// Evaluated values of exported declarations whose runtime binding is
64    /// produced by module initialization, including structs and enums.
65    pub(crate) public_values: BTreeMap<String, VmValue>,
66    /// Decoded JSON-Schema dict for each `pub type` alias that lowers to a
67    /// schema. Importers bind the alias name to this value so
68    /// expression-position uses (`output: ImportedAlias`) work.
69    pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
70    /// Guard under which this filesystem module and its transitive closure were
71    /// instantiated. A guarded execution cannot reuse an unguarded module even
72    /// when the entry bytes currently match: its closures may retain imports
73    /// compiled from earlier, unverified bytes.
74    package_execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
75    pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
76    pub(crate) _module_state: crate::value::ModuleState,
77}
78
79/// Runtime module cache shared by child VMs within one execution tree.
80///
81/// The map stays copy-on-write so a child can add modules without mutating its
82/// parent. Cache entries are never replaced after instantiation, so cache hits
83/// and map copies share their export maps plus their existing shared
84/// registries/state through a cheap outer [`Arc`] instead of cloning the whole
85/// module.
86pub(crate) type ModuleCache = Arc<BTreeMap<PathBuf, Arc<LoadedModule>>>;
87
88/// An import whose target module was still mid-load (an import cycle) when the
89/// importing module reached it. The target's function closures don't exist yet
90/// at that point, so the binding can't happen inline. We record it here and
91/// resolve it once both modules are fully loaded — see
92/// [`Vm::flush_deferred_cyclic_imports`].
93#[derive(Clone, Debug)]
94pub(crate) struct DeferredCyclicImport {
95    /// Canonical path of the module that issued the import.
96    pub(crate) importer: PathBuf,
97    /// Canonical path of the cyclically-imported target module.
98    pub(crate) target: PathBuf,
99    /// Selectively-imported names, or `None` for a wildcard/side-effect import.
100    pub(crate) selected_names: Option<Vec<String>>,
101    /// When set, bind a namespace dict under this alias instead of flattening.
102    pub(crate) namespace_alias: Option<String>,
103    /// Statically demanded namespace members, or `None` for the whole namespace.
104    pub(crate) namespace_members: Option<Vec<String>>,
105}
106
107#[derive(Clone, Copy)]
108enum ImportProjection<'a> {
109    BindCaller(Option<&'a [String]>),
110    /// Bind `import * as alias` as a single namespace dict.
111    BindNamespace(&'a str, Option<&'a [String]>),
112    MaterializeOnly,
113}
114
115impl ImportProjection<'_> {
116    fn package_rejection_kind(self) -> &'static str {
117        match self {
118            Self::BindCaller(_) | Self::BindNamespace(..) => "import",
119            Self::MaterializeOnly => "execution",
120        }
121    }
122}
123
124/// Resolve the names an import may introduce from one loaded module. The
125/// artifact's typed export contract is authoritative for ordinary imports,
126/// re-exports, and delayed cycle binding alike.
127#[derive(Clone, Copy)]
128enum ImportNameUse {
129    Binding,
130    Namespace,
131}
132
133fn module_import_names(
134    module_name: &str,
135    loaded: &LoadedModule,
136    selected_names: Option<&[String]>,
137    name_use: ImportNameUse,
138) -> Result<Vec<String>, VmError> {
139    if let Some(names) = selected_names {
140        for name in names {
141            if !loaded.public_exports.contains_key(name) {
142                let message = match name_use {
143                    ImportNameUse::Binding => {
144                        let hint = if loaded.functions.contains_key(name) {
145                            " — it is defined there but not `pub`; mark it `pub` to export it"
146                        } else {
147                            ""
148                        };
149                        format!("Import error: '{name}' is not exported by {module_name}{hint}")
150                    }
151                    ImportNameUse::Namespace => {
152                        format!("module `{module_name}` has no exported member `{name}`")
153                    }
154                };
155                return Err(VmError::Runtime(message));
156            }
157        }
158        return Ok(names.to_vec());
159    }
160
161    Ok(loaded.public_exports.keys().cloned().collect())
162}
163
164/// Build the closed namespace dict for `import * as alias from path`.
165///
166/// Includes `"_namespace" → module path` so `call_dict_method` dispatches
167/// callable fields, plus every public export that has a runtime value.
168/// Type/interface-only exports are omitted (no runtime binding).
169fn build_namespace_dict(
170    module_path: &str,
171    loaded: &LoadedModule,
172    members: Option<&[String]>,
173) -> Result<VmValue, VmError> {
174    let mut map = BTreeMap::new();
175    map.insert(
176        "_namespace".to_string(),
177        VmValue::String(arcstr::ArcStr::from(module_path)),
178    );
179    let names = module_import_names(module_path, loaded, members, ImportNameUse::Namespace)?;
180    for name in names {
181        let kind = loaded
182            .public_exports
183            .get(&name)
184            .expect("module_import_names validates the public export contract");
185        if !kind.has_runtime_value() {
186            // Still project schema-capable type aliases when present.
187            if let Some(schema) = loaded.public_type_schemas.get(&name) {
188                map.insert(name, schema.clone());
189            }
190            continue;
191        }
192        if let Some(value) = loaded.public_values.get(&name) {
193            map.insert(name, value.clone());
194            continue;
195        }
196        if let Some(schema) = loaded.public_type_schemas.get(&name) {
197            map.insert(name, schema.clone());
198            continue;
199        }
200        if let Some(closure) = loaded.functions.get(&name) {
201            map.insert(name, VmValue::Closure(Arc::clone(closure)));
202        }
203    }
204    Ok(VmValue::dict(map))
205}
206
207pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
208    let synthetic_current_file = base.join("__harn_import_base__.harn");
209    if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
210        return resolved;
211    }
212
213    let mut file_path = base.join(path);
214
215    if !file_path.exists() && file_path.extension().is_none() {
216        file_path.set_extension("harn");
217    }
218
219    file_path
220}
221
222impl Vm {
223    /// Dedicate this fresh VM to a Rust embedder-owned host-dispatch graph.
224    ///
225    /// The transition is one-way and must happen before any module load. The
226    /// trusted graph bypasses ordinary prepared/on-disk bytecode caches and
227    /// only the embedder can select a callable from it.
228    pub fn enable_trusted_host_dispatch(&mut self) -> Result<(), VmError> {
229        self.ensure_execution_available()?;
230        if self.module_provenance == ModuleProvenance::TrustedHostDispatch {
231            return Ok(());
232        }
233        if !self.module_cache.is_empty() || !self.imported_paths.is_empty() {
234            return Err(VmError::Runtime(
235                "trusted host dispatch must be enabled before loading modules".to_string(),
236            ));
237        }
238        self.module_provenance = ModuleProvenance::TrustedHostDispatch;
239        self.graph_link_table = None;
240        self.linked_program_repository = None;
241        Ok(())
242    }
243
244    fn resolve_module_import_path(&self, base: &Path, path: &str) -> Result<PathBuf, VmError> {
245        if let Some(guard) = &self.package_execution_guard {
246            let synthetic_current_file = base.join("__harn_import_base__.harn");
247            if let Some(resolved) =
248                harn_modules::resolve_import_path_with_guard(&synthetic_current_file, path, guard)
249                    .map_err(|error| {
250                    VmError::Runtime(format!("installed package import rejected: {error}"))
251                })?
252            {
253                return Ok(resolved);
254            }
255            let mut file_path = base.join(path);
256            if !file_path.exists() && file_path.extension().is_none() {
257                file_path.set_extension("harn");
258            }
259            return Ok(file_path);
260        }
261        Ok(resolve_module_import_path(base, path))
262    }
263
264    /// Resolve a callable against this VM. Lazy callables initialize once per
265    /// VM execution tree, then retain that module scope for later child VMs in
266    /// the same tree. Fresh VM roots remain isolated.
267    pub async fn resolve_callable(
268        &mut self,
269        callable: &crate::value::VmCallable,
270    ) -> Result<Arc<crate::value::VmClosure>, VmError> {
271        self.ensure_execution_available()?;
272        match callable {
273            crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
274            crate::value::VmCallable::Lazy(lazy) => {
275                lazy.prepare().await?;
276                let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
277                let next_guard = lazy
278                    .package_execution_guard_handle()
279                    .or_else(|| self.package_execution_guard.clone());
280                if let Some(guard) = &next_guard {
281                    guard.verify_entry_source(&module_path).map_err(|error| {
282                        VmError::Runtime(format!("installed package execution rejected: {error}"))
283                    })?;
284                }
285                let resolution = {
286                    let mut modules = self.lazy_callable_modules.lock();
287                    let slots = modules.entry(cache_key).or_default();
288                    if let Some(slot) = slots.iter().find(|slot| slot.execution_guard == next_guard)
289                    {
290                        Arc::clone(&slot.resolution)
291                    } else {
292                        let resolution = Arc::new(tokio::sync::OnceCell::new());
293                        slots.push(crate::vm::state::LazyCallableCacheSlot {
294                            execution_guard: next_guard.clone(),
295                            resolution: Arc::clone(&resolution),
296                        });
297                        resolution
298                    }
299                };
300                let previous_package_execution_guard =
301                    std::mem::replace(&mut self.package_execution_guard, next_guard);
302                let resolved = resolution
303                    .get_or_try_init(|| async {
304                        let exports = self.load_module_exports(&module_path).await?;
305                        let exports = exports
306                            .into_iter()
307                            .map(|(name, closure)| (name, closure.retained_for_host_registry()))
308                            .collect();
309                        // Pin the complete module graph loaded above so that a
310                        // handler's transitively imported callees keep their
311                        // home-module registries/state alive for later child
312                        // VMs that hit this cache without re-importing.
313                        Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
314                            exports,
315                            retained_module_graph: Arc::clone(&self.module_cache),
316                        }))
317                    })
318                    .await;
319                self.package_execution_guard = previous_package_execution_guard;
320                let resolved = resolved?;
321                resolved
322                    .exports
323                    .get(&lazy.function_name)
324                    .cloned()
325                    .ok_or_else(|| {
326                        VmError::Runtime(format!(
327                            "function '{}' is not exported by module '{}'",
328                            lazy.function_name,
329                            lazy.module_path.display()
330                        ))
331                    })
332            }
333            crate::value::VmCallable::Pipeline(_) => Err(VmError::TypeError(
334                "pipeline callable requires execute_callable".to_string(),
335            )),
336        }
337    }
338
339    pub async fn execute_callable(
340        &mut self,
341        callable: &crate::value::VmCallable,
342        args: &[crate::value::VmValue],
343    ) -> Result<crate::value::VmValue, VmError> {
344        let crate::value::VmCallable::Pipeline(pipeline) = callable else {
345            let closure = self.resolve_callable(callable).await?;
346            return self.call_closure_pub(&closure, args).await;
347        };
348
349        let (_, module_path) = self.lazy_module_path(&pipeline.module_path);
350        let next_guard = pipeline
351            .package_execution_guard_handle()
352            .or_else(|| self.package_execution_guard.clone());
353        let previous_package_execution_guard =
354            std::mem::replace(&mut self.package_execution_guard, next_guard);
355        let result = async {
356            let closure = self
357                .load_public_module_callable(&module_path, &pipeline.pipeline_name)
358                .await?;
359            self.call_closure_pub(&closure, args).await
360        }
361        .await;
362        self.package_execution_guard = previous_package_execution_guard;
363        result
364    }
365
366    fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
367        self.lazy_module_path(&lazy.module_path)
368    }
369
370    fn lazy_module_path(&self, path: &std::path::Path) -> (PathBuf, PathBuf) {
371        let mut module_path = if path.is_absolute() {
372            path.to_path_buf()
373        } else {
374            self.source_dir
375                .clone()
376                .unwrap_or_else(|| PathBuf::from("."))
377                .join(path)
378        };
379        if !module_path.exists() && module_path.extension().is_none() {
380            module_path.set_extension("harn");
381        }
382        let cache_key = module_path
383            .canonicalize()
384            .unwrap_or_else(|_| module_path.clone());
385        (cache_key, module_path)
386    }
387
388    async fn load_module_from_source(
389        &mut self,
390        synthetic: PathBuf,
391        source: &str,
392    ) -> Result<Arc<LoadedModule>, VmError> {
393        self.load_module_from_source_with_preparation(synthetic, source, false)
394            .await
395    }
396
397    async fn load_module_from_source_with_preparation(
398        &mut self,
399        synthetic: PathBuf,
400        source: &str,
401        use_prepared_generation: bool,
402    ) -> Result<Arc<LoadedModule>, VmError> {
403        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
404            return Ok(loaded);
405        }
406        let source = Arc::new(ModuleSource::from_text(source));
407        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), Arc::clone(source.text()));
408        let artifact = if use_prepared_generation {
409            self.prepared_module_cache.prepare(
410                &synthetic,
411                &synthetic,
412                &source,
413                None,
414                self.module_phase_recorder.as_ref(),
415                self.module_provenance,
416                &self.prepared_module_validation,
417            )?
418        } else {
419            let mut compile_span = self.module_compile_span();
420            let compiled = match self.module_provenance {
421                ModuleProvenance::EmbeddedStdlib => {
422                    compile_embedded_stdlib_module_artifact_from_source(
423                        &synthetic,
424                        source.as_str(),
425                    )?
426                }
427                ModuleProvenance::TrustedHostDispatch => {
428                    compile_trusted_host_dispatch_module_artifact_from_source(
429                        &synthetic,
430                        source.as_str(),
431                    )?
432                }
433                ModuleProvenance::User | ModuleProvenance::PrivilegedWire => {
434                    compile_module_artifact_from_source(&synthetic, source.as_str())?
435                }
436            };
437            if let Some(span) = &mut compile_span {
438                span.mark_compile_succeeded();
439            }
440            drop(compile_span);
441            let _load_span = self.module_load_span();
442            Arc::new(PreparedModuleArtifact::from_cached(compiled))
443        };
444
445        self.imported_paths.push(synthetic.clone());
446        let source_dir = use_prepared_generation
447            .then(|| synthetic.parent().map(Path::to_path_buf))
448            .flatten();
449        let loaded = Arc::new(self.instantiate_module(source_dir, &artifact).await?);
450        self.imported_paths.pop();
451        {
452            let _load_span = self.module_load_span();
453            Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
454        }
455        self.record_module_loaded();
456        Ok(loaded)
457    }
458
459    /// Widen a stdlib module's export surface with the builtins it re-exports
460    /// (see [`harn_stdlib::builtin_reexports`]), so a Rust-implemented member of
461    /// the module imports exactly like a Harn-implemented one.
462    ///
463    /// The name binds to a [`VmValue::BuiltinRef`], which is what a bare mention
464    /// of a builtin already evaluates to — so an imported `assert_eq` and a
465    /// global `assert_eq` are the same function reached two ways, not two
466    /// implementations that can drift.
467    fn add_builtin_reexports(module: &str, loaded: &mut LoadedModule) {
468        for name in harn_stdlib::builtin_reexports(module) {
469            // A `pub fn` in the module's Harn source wins: it is the more
470            // specific declaration, and silently shadowing it here would make
471            // the source of an export unguessable from reading the module.
472            if loaded.public_exports.contains_key(*name) {
473                continue;
474            }
475            loaded
476                .public_exports
477                .insert((*name).to_string(), DefKind::Function);
478            loaded.public_values.insert(
479                (*name).to_string(),
480                VmValue::BuiltinRef(arcstr::ArcStr::from(*name)),
481            );
482        }
483    }
484
485    async fn load_stdlib_module_from_source(
486        &mut self,
487        module: &str,
488        synthetic: PathBuf,
489        source: &'static str,
490    ) -> Result<Arc<LoadedModule>, VmError> {
491        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
492            return Ok(loaded);
493        }
494        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), Arc::from(source));
495
496        let artifact = stdlib_module_artifact(
497            module,
498            &synthetic,
499            source,
500            self.module_phase_recorder.as_ref(),
501        )?;
502        self.imported_paths.push(synthetic.clone());
503        let mut loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
504        self.imported_paths.pop();
505        Self::add_builtin_reexports(module, &mut loaded);
506        let loaded = Arc::new(loaded);
507        {
508            let _load_span = self.module_load_span();
509            Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
510        }
511        self.record_module_loaded();
512        Ok(loaded)
513    }
514
515    async fn instantiate_stdlib_module(
516        &mut self,
517        artifact: &PreparedModuleArtifact,
518    ) -> Result<LoadedModule, VmError> {
519        self.instantiate_module(None, artifact).await
520    }
521
522    /// Instantiate a previously-hydrated [`PreparedModuleArtifact`] into a
523    /// [`LoadedModule`]. Re-runs nested imports, replays the init chunk
524    /// into a fresh module env, mints a [`VmClosure`] for each compiled
525    /// function (stamped with `module_source_dir` so imports from inside
526    /// those functions resolve against the originating file), and
527    /// applies the re-export pass. Used by both stdlib and user-import
528    /// code paths.
529    async fn instantiate_module(
530        &mut self,
531        module_source_dir: Option<PathBuf>,
532        artifact: &PreparedModuleArtifact,
533    ) -> Result<LoadedModule, VmError> {
534        let caller_env = self.env.clone();
535        let old_source_dir = self.source_dir.clone();
536        self.env = VmEnv::new();
537        self.source_dir = module_source_dir.clone();
538
539        for import in &artifact.imports {
540            let projection = match &import.binding {
541                ModuleImportBinding::Wildcard => ImportProjection::BindCaller(None),
542                ModuleImportBinding::Selected(names) => ImportProjection::BindCaller(Some(names)),
543                ModuleImportBinding::Namespace { alias, demand } => {
544                    let members = match demand {
545                        harn_parser::NamespaceDemand::Whole => None,
546                        harn_parser::NamespaceDemand::Members(members) => {
547                            Some(members.iter().cloned().collect::<Vec<_>>())
548                        }
549                    };
550                    self.execute_import_with_projection(
551                        &import.path,
552                        ImportProjection::BindNamespace(alias, members.as_deref()),
553                        artifact.provenance,
554                    )
555                    .await?;
556                    continue;
557                }
558            };
559            self.execute_import_with_projection(&import.path, projection, artifact.provenance)
560                .await?;
561        }
562
563        // Nested modules own their own load spans. Start this module's span
564        // only after those imports finish so aggregate load time is additive.
565        let _load_span = self.module_load_span();
566
567        let module_state: crate::value::ModuleState = {
568            let mut init_env = self.env.clone();
569            if !artifact.type_schema_init_chunks.is_empty() || artifact.init_chunk.is_some() {
570                let saved_env = std::mem::replace(&mut self.env, init_env);
571                let saved_frames = std::mem::take(&mut self.frames);
572                let saved_handlers = std::mem::take(&mut self.exception_handlers);
573                let saved_iterators = std::mem::take(&mut self.iterators);
574                let saved_deadlines = std::mem::take(&mut self.deadlines);
575                // STEP_STACK / PERSONA_STACK are thread-locals shared with
576                // the calling frame. Emptying `self.frames` above means
577                // any `prune_below_frame(0)` triggered while the init
578                // chunk's bytecode runs — including the inevitable
579                // frame-pop prune at end-of-chunk — would wipe active
580                // steps owned by the *caller* (e.g., a `@step`-decorated
581                // function whose body lazily imports a module). Snapshot
582                // the persona/step context here and restore it after init
583                // so module loading is invisible to the step-tracking
584                // surface.
585                let active_context = crate::step_runtime::suspend_active_context();
586                let init_result: Result<(), VmError> = async {
587                    for chunk in &artifact.type_schema_init_chunks {
588                        self.run_chunk(Arc::clone(chunk)).await?;
589                    }
590                    if let Some(chunk) = &artifact.init_chunk {
591                        self.run_chunk(Arc::clone(chunk)).await?;
592                    }
593                    Ok(())
594                }
595                .await;
596                drop(active_context);
597                init_env = std::mem::replace(&mut self.env, saved_env);
598                self.frames = saved_frames;
599                self.exception_handlers = saved_handlers;
600                self.iterators = saved_iterators;
601                self.deadlines = saved_deadlines;
602                init_result?;
603            }
604            Arc::new(crate::value::VmMutex::new(init_env))
605        };
606
607        let module_env = self.env.clone();
608        let registry: ModuleFunctionRegistry =
609            Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
610        let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
611        let mut public_exports = artifact.public_exports.clone();
612        // The init chunk already ran into `module_state`, so init-backed public
613        // values are live there. Read only the names identified by the artifact
614        // contract and publish their evaluated values for importers.
615        let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
616        {
617            let state = module_state.lock();
618            for name in &artifact.public_value_names {
619                if let Some(value) = state.get(name) {
620                    public_values.insert(name.clone(), value);
621                }
622            }
623        }
624        if artifact.provenance == crate::module_artifact::ModuleProvenance::PrivilegedWire {
625            for (name, value) in &public_values {
626                if !matches!(value, VmValue::Harness(_)) {
627                    return Err(VmError::Runtime(format!(
628                        "Privileged wire module export `{name}` produced {}; only a nominal Harness capability handle may cross the wire boundary",
629                        value.type_name()
630                    )));
631                }
632            }
633        }
634        let public_type_names = artifact.public_type_names.clone();
635        let mut public_type_schemas: BTreeMap<String, VmValue> = {
636            let state = module_state.lock();
637            public_type_names
638                .iter()
639                .filter_map(|name| state.get(name).map(|schema| (name.clone(), schema)))
640                .collect()
641        };
642
643        for (name, compiled) in &artifact.functions {
644            let closure = Arc::new(VmClosure {
645                func: Arc::clone(compiled),
646                env: module_env.clone(),
647                source_dir: module_source_dir.clone(),
648                module_functions: Some(Arc::downgrade(&registry)),
649                module_state: Some(Arc::downgrade(&module_state)),
650                retained_module_scope: None,
651            });
652            registry.lock().insert(name.clone(), Arc::clone(&closure));
653            self.env
654                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
655            module_state
656                .lock()
657                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
658            functions.insert(name.clone(), Arc::clone(&closure));
659        }
660
661        for import in artifact.imports.iter().filter(|import| import.is_pub) {
662            let cache_key = self.cache_key_for_import(&import.path)?;
663            let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
664                // A plain `import`/`import {...}` across a cycle is bound late
665                // by `flush_deferred_cyclic_imports`, but a `pub import`
666                // re-export has to publish the names into *this* module's
667                // public surface right now — and the target is still mid-load,
668                // so its surface does not exist yet. Name the cycle explicitly
669                // instead of the misleading "was not loaded".
670                if self.imported_paths.contains(&cache_key) {
671                    return Err(VmError::Runtime(format!(
672                        "Re-export error: cannot `pub import` from '{}' because it forms an \
673                         import cycle with this module (its public surface is still being \
674                         built). Use a plain `import` here, or re-export from a module that is \
675                         not part of the cycle.",
676                        import.path
677                    )));
678                }
679                return Err(VmError::Runtime(format!(
680                    "Re-export error: imported module '{}' was not loaded",
681                    import.path
682                )));
683            };
684            // `pub import * as alias` publishes the alias namespace object —
685            // never flatten target members into this module's public surface.
686            if let ModuleImportBinding::Namespace { alias, .. } = &import.binding {
687                if public_exports.contains_key(alias) || functions.contains_key(alias) {
688                    return Err(VmError::Runtime(format!(
689                        "Re-export collision: '{alias}' is defined here and also \
690                         re-exported as a namespace from '{}'",
691                        import.path
692                    )));
693                }
694                // A public namespace is observable as a first-class value and
695                // is therefore always complete.
696                let dict = build_namespace_dict(&import.path, &loaded, None)?;
697                public_values.insert(alias.clone(), dict);
698                public_exports.insert(alias.clone(), DefKind::Variable);
699                continue;
700            }
701            let selected_names = match &import.binding {
702                ModuleImportBinding::Selected(names) => Some(names.as_slice()),
703                ModuleImportBinding::Wildcard => None,
704                ModuleImportBinding::Namespace { .. } => unreachable!("handled above"),
705            };
706            let names_to_reexport = module_import_names(
707                &import.path,
708                &loaded,
709                selected_names,
710                ImportNameUse::Binding,
711            )?;
712            for name in names_to_reexport {
713                let Some(kind) = loaded.public_exports.get(&name).copied() else {
714                    return Err(VmError::Runtime(format!(
715                        "Re-export error: '{name}' is not exported by '{}'",
716                        import.path
717                    )));
718                };
719                let Some(closure) = loaded.functions.get(&name) else {
720                    // Init-backed declarations carry their evaluated value
721                    // directly, including struct constructors and enum
722                    // namespaces.
723                    if let Some(value) = loaded.public_values.get(&name) {
724                        public_values.insert(name.clone(), value.clone());
725                        public_exports.insert(name, kind);
726                        continue;
727                    }
728                    // Type-only declarations carry no runtime binding. Preserve
729                    // an optional schema lowering and the contract entry.
730                    if let Some(schema) = loaded.public_type_schemas.get(&name) {
731                        public_type_schemas.insert(name.clone(), schema.clone());
732                    }
733                    public_exports.insert(name, kind);
734                    continue;
735                };
736                if let Some(existing) = functions.get(&name) {
737                    if !Arc::ptr_eq(existing, closure) {
738                        return Err(VmError::Runtime(format!(
739                            "Re-export collision: '{name}' is defined here and also \
740                             re-exported from '{}'",
741                            import.path
742                        )));
743                    }
744                }
745                functions.insert(name.clone(), Arc::clone(closure));
746                public_exports.insert(name, kind);
747            }
748        }
749
750        self.env = caller_env;
751        self.source_dir = old_source_dir;
752
753        Ok(LoadedModule {
754            functions,
755            public_exports,
756            public_values,
757            public_type_schemas,
758            package_execution_guard: module_source_dir
759                .as_ref()
760                .and(self.package_execution_guard.clone()),
761            _module_functions: registry,
762            _module_state: module_state,
763        })
764    }
765
766    fn export_namespace_module(
767        &mut self,
768        module_path: &Path,
769        loaded: &LoadedModule,
770        alias: &str,
771        members: Option<&[String]>,
772    ) -> Result<(), VmError> {
773        let module_name = module_path.display().to_string();
774        if self.env.get(alias).is_some() {
775            return Err(VmError::Runtime(format!(
776                "Import collision: '{alias}' is already defined when importing {module_name}. \
777                 Use a different namespace alias: import * as <name> from \"...\""
778            )));
779        }
780        let dict = build_namespace_dict(&module_name, loaded, members)?;
781        self.env.define(alias, dict, false)?;
782        Ok(())
783    }
784
785    fn export_loaded_module(
786        &mut self,
787        module_path: &Path,
788        loaded: &LoadedModule,
789        selected_names: Option<&[String]>,
790    ) -> Result<(), VmError> {
791        let module_name = module_path.display().to_string();
792        let export_names =
793            module_import_names(&module_name, loaded, selected_names, ImportNameUse::Binding)?;
794
795        for name in export_names {
796            // `pub const` / `pub let` values: bind by value.
797            if let Some(value) = loaded.public_values.get(&name) {
798                if self.env.get(&name).is_some() {
799                    return Err(VmError::Runtime(format!(
800                        "Import collision: '{name}' is already defined when importing \
801                         {module_name}. Use selective imports to disambiguate: \
802                         import {{ {name} }} from \"...\""
803                    )));
804                }
805                self.env.define(&name, value.clone(), false)?;
806                continue;
807            }
808            // Type and interface declarations are valid imports without a
809            // runtime value. Schema-capable aliases still bind their schema so
810            // expression-position uses match local alias lowering.
811            if let Some(schema) = loaded.public_type_schemas.get(&name) {
812                self.env.define(&name, schema.clone(), false)?;
813                continue;
814            }
815            if loaded
816                .public_exports
817                .get(&name)
818                .is_some_and(|kind| !kind.has_runtime_value())
819            {
820                continue;
821            }
822            let Some(closure) = loaded.functions.get(&name) else {
823                return Err(VmError::Runtime(format!(
824                    "Import error: '{name}' is not defined in {module_name}"
825                )));
826            };
827            if let Some(VmValue::Closure(_)) = self.env.get(&name) {
828                return Err(VmError::Runtime(format!(
829                    "Import collision: '{name}' is already defined when importing {module_name}. \
830                     Use selective imports to disambiguate: import {{ {name} }} from \"...\""
831                )));
832            }
833            self.env
834                .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
835        }
836        Ok(())
837    }
838
839    /// Execute an import, reading and running the file's declarations.
840    pub(super) fn execute_import<'a>(
841        &'a mut self,
842        path: &'a str,
843        selected_names: Option<&'a [String]>,
844    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
845        self.execute_import_with_projection(
846            path,
847            ImportProjection::BindCaller(selected_names),
848            self.module_provenance,
849        )
850    }
851
852    /// Bind `import * as alias from path` as a closed namespace dict.
853    pub(super) fn execute_namespace_import_bind<'a>(
854        &'a mut self,
855        path: &'a str,
856        alias: &'a str,
857        members: Option<&'a [String]>,
858    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
859        self.execute_import_with_projection(
860            path,
861            ImportProjection::BindNamespace(alias, members),
862            self.module_provenance,
863        )
864    }
865
866    fn materialize_import<'a>(
867        &'a mut self,
868        path: &'a str,
869    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
870        self.execute_import_with_projection(
871            path,
872            ImportProjection::MaterializeOnly,
873            self.module_provenance,
874        )
875    }
876
877    fn apply_import_projection(
878        &mut self,
879        module_path: &Path,
880        loaded: &LoadedModule,
881        projection: ImportProjection<'_>,
882    ) -> Result<(), VmError> {
883        match projection {
884            ImportProjection::BindCaller(selected_names) => {
885                self.export_loaded_module(module_path, loaded, selected_names)
886            }
887            ImportProjection::BindNamespace(alias, members) => {
888                self.export_namespace_module(module_path, loaded, alias, members)
889            }
890            ImportProjection::MaterializeOnly => Ok(()),
891        }
892    }
893
894    fn execute_import_with_projection<'a>(
895        &'a mut self,
896        path: &'a str,
897        projection: ImportProjection<'a>,
898        provenance: ModuleProvenance,
899    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
900        Box::pin(async move {
901            let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
902
903            let stdlib_module = path
904                .strip_prefix("std/")
905                .or_else(|| (path == "observability").then_some("observability"));
906            if let Some(module) = stdlib_module {
907                if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
908                    let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
909                    if self.imported_paths.contains(&synthetic) {
910                        return Ok(());
911                    }
912                    if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
913                        return self.apply_import_projection(&synthetic, &loaded, projection);
914                    }
915                    if let Some(repository) = &self.linked_program_repository {
916                        let artifact = repository.get(&synthetic).ok_or_else(|| {
917                            VmError::Runtime(format!(
918                                "linked program is missing required module std/{module}"
919                            ))
920                        })?;
921                        self.imported_paths.push(synthetic.clone());
922                        let loaded = Arc::new(
923                            self.instantiate_module(
924                                synthetic.parent().map(Path::to_path_buf),
925                                &artifact,
926                            )
927                            .await?,
928                        );
929                        self.imported_paths.pop();
930                        Arc::make_mut(&mut self.module_cache)
931                            .insert(synthetic.clone(), Arc::clone(&loaded));
932                        self.record_module_loaded();
933                        return self.apply_import_projection(&synthetic, &loaded, projection);
934                    }
935                    let loaded = self
936                        .load_stdlib_module_from_source(module, synthetic.clone(), source)
937                        .await?;
938                    if !matches!(projection, ImportProjection::MaterializeOnly) {
939                        let _load_span = self.module_load_span();
940                        self.apply_import_projection(&synthetic, &loaded, projection)?;
941                    }
942                    return Ok(());
943                }
944                return Err(VmError::Runtime(format!(
945                    "Unknown stdlib module: std/{module}"
946                )));
947            }
948
949            let base = self
950                .source_dir
951                .clone()
952                .unwrap_or_else(|| PathBuf::from("."));
953            let file_path = self.resolve_module_import_path(&base, path)?;
954            let verified_source = if let Some(guard) = &self.package_execution_guard {
955                let bytes = guard.verify_entry_source(&file_path).map_err(|error| {
956                    VmError::Runtime(format!(
957                        "installed package {} rejected: {error}",
958                        projection.package_rejection_kind()
959                    ))
960                })?;
961                Some(verified_package_source(bytes, &file_path)?)
962            } else {
963                None
964            };
965
966            let canonical = harn_modules::canonical_path(&file_path);
967            if self.imported_paths.contains(&canonical) {
968                // Import cycle: `canonical` is still mid-load (it sits on the
969                // import stack), so its function closures don't exist yet and
970                // we cannot bind the requested names inline. Record the import
971                // and resolve it once both modules finish loading — otherwise
972                // whichever module happens to close the cycle silently loses
973                // these bindings and fails with `Undefined builtin` at call
974                // time, in a load-order-dependent way.
975                match projection {
976                    ImportProjection::BindCaller(selected_names) => {
977                        if let Some(importer) = self.imported_paths.last().cloned() {
978                            if importer != canonical {
979                                self.deferred_cyclic_imports.push(DeferredCyclicImport {
980                                    importer,
981                                    target: canonical.clone(),
982                                    selected_names: selected_names.map(<[String]>::to_vec),
983                                    namespace_alias: None,
984                                    namespace_members: None,
985                                });
986                            }
987                        }
988                    }
989                    ImportProjection::BindNamespace(alias, members) => {
990                        if let Some(importer) = self.imported_paths.last().cloned() {
991                            if importer != canonical {
992                                self.deferred_cyclic_imports.push(DeferredCyclicImport {
993                                    importer,
994                                    target: canonical.clone(),
995                                    selected_names: None,
996                                    namespace_alias: Some(alias.to_string()),
997                                    namespace_members: members.map(<[String]>::to_vec),
998                                });
999                            }
1000                        }
1001                    }
1002                    ImportProjection::MaterializeOnly => {}
1003                }
1004                return Ok(());
1005            }
1006            if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
1007                if let Some(source) = &verified_source {
1008                    let cached_source = self.source_cache.get(&canonical).map(Arc::as_ref);
1009                    if cached_source != Some(source.as_str()) {
1010                        return Err(VmError::Runtime(format!(
1011                            "installed package {} rejected: cached module {} was not compiled from the verified package bytes",
1012                            projection.package_rejection_kind(),
1013                            canonical.display()
1014                        )));
1015                    }
1016                    let active_guard = self
1017                        .package_execution_guard
1018                        .as_deref()
1019                        .expect("verified package source requires an active guard");
1020                    if loaded.package_execution_guard.as_deref() != Some(active_guard) {
1021                        return Err(VmError::Runtime(format!(
1022                            "installed package {} rejected: cached module {} was not instantiated under the active package execution guard",
1023                            projection.package_rejection_kind(),
1024                            canonical.display()
1025                        )));
1026                    }
1027                }
1028                return self.apply_import_projection(&canonical, &loaded, projection);
1029            }
1030            self.imported_paths.push(canonical.clone());
1031
1032            // The link table, when this graph has one, names this module's
1033            // artifact from a digest the entry walk already recorded — so the
1034            // file is never read. Guard-verified package bytes are excluded:
1035            // they are their own authority and deliberately bypass every memo,
1036            // and `verified_source` is `Some` exactly when a guard is active.
1037            let closed = self
1038                .linked_program_repository
1039                .as_ref()
1040                .map(|repository| {
1041                    repository
1042                        .get(&canonical)
1043                        .or_else(|| repository.get(&file_path))
1044                        .ok_or_else(|| {
1045                            VmError::Runtime(format!(
1046                                "linked program is missing required module {}",
1047                                file_path.display()
1048                            ))
1049                        })
1050                })
1051                .transpose()?;
1052
1053            let linked = (closed.is_none()
1054                && provenance == ModuleProvenance::User
1055                && verified_source.is_none())
1056            .then(|| {
1057                let (content_hash, compilation_context) = self
1058                    .graph_link_table
1059                    .as_ref()?
1060                    .module_identity(canonical.as_path())?;
1061                let _load_span = self.module_load_span();
1062                self.linked_module_artifact(
1063                    &file_path,
1064                    &canonical,
1065                    content_hash,
1066                    &compilation_context,
1067                )
1068            })
1069            .flatten();
1070
1071            let artifact = if let Some(closed) = closed {
1072                closed
1073            } else if let Some(linked) = linked {
1074                linked
1075            } else {
1076                let source = {
1077                    let _load_span = self.module_load_span();
1078                    match verified_source {
1079                        // Guard-verified package bytes are their own authority
1080                        // and never come from the shared on-disk memo.
1081                        Some(source) => Arc::new(ModuleSource::from_text(source)),
1082                        None => self
1083                            .source_cache
1084                            .get(&canonical)
1085                            .cloned()
1086                            .map(ModuleSource::from_text)
1087                            .map(Arc::new)
1088                            .map(Ok)
1089                            .unwrap_or_else(|| module_source::read(&file_path))
1090                            .map_err(|e| {
1091                                // Name the resolution base: relative imports resolve against
1092                                // the importing file's dir (or CWD when unset), so an error
1093                                // that prints only the joined path leaves the author guessing
1094                                // which base was used.
1095                                VmError::Runtime(format!(
1096                                    "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
1097                                    file_path.display(),
1098                                    base.display()
1099                                ))
1100                            })?,
1101                    }
1102                };
1103                {
1104                    let source_is_cached = |path: &Path| {
1105                        self.source_cache
1106                            .get(path)
1107                            .is_some_and(|cached| cached.as_ref() == source.as_str())
1108                    };
1109                    if !source_is_cached(&canonical) || !source_is_cached(&file_path) {
1110                        let source_cache = Arc::make_mut(&mut self.source_cache);
1111                        source_cache.insert(canonical.clone(), Arc::clone(source.text()));
1112                        source_cache.insert(file_path.clone(), Arc::clone(source.text()));
1113                    }
1114                }
1115
1116                match provenance {
1117                    ModuleProvenance::TrustedHostDispatch => self.prepared_module_cache.prepare(
1118                        &file_path,
1119                        &canonical,
1120                        &source,
1121                        None,
1122                        self.module_phase_recorder.as_ref(),
1123                        ModuleProvenance::TrustedHostDispatch,
1124                        &self.prepared_module_validation,
1125                    )?,
1126                    ModuleProvenance::User
1127                    | ModuleProvenance::EmbeddedStdlib
1128                    | ModuleProvenance::PrivilegedWire => self.prepared_module_cache.prepare(
1129                        &file_path,
1130                        &canonical,
1131                        &source,
1132                        None,
1133                        self.module_phase_recorder.as_ref(),
1134                        ModuleProvenance::User,
1135                        &self.prepared_module_validation,
1136                    )?,
1137                }
1138            };
1139
1140            let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
1141            let loaded = Arc::new(
1142                self.instantiate_module(module_source_dir, artifact.as_ref())
1143                    .await?,
1144            );
1145            self.imported_paths.pop();
1146            {
1147                let _load_span = self.module_load_span();
1148                Arc::make_mut(&mut self.module_cache)
1149                    .insert(canonical.clone(), Arc::clone(&loaded));
1150            }
1151            self.record_module_loaded();
1152            if !matches!(projection, ImportProjection::MaterializeOnly) {
1153                let _load_span = self.module_load_span();
1154                self.apply_import_projection(&canonical, &loaded, projection)?;
1155            }
1156
1157            // Once the import stack fully unwinds, every module reachable from
1158            // this top-level import is cached, so any deferred cyclic imports
1159            // can now bind against fully-loaded modules.
1160            if self.imported_paths.is_empty() {
1161                let _load_span = self.module_load_span();
1162                self.flush_deferred_cyclic_imports()?;
1163            }
1164
1165            Ok(())
1166        })
1167    }
1168
1169    /// Resolve a module the link table names, without reading its source.
1170    ///
1171    /// `content_hash` comes from a manifest that was re-checked before the table
1172    /// was built, so it describes the bytes currently on disk. That is the whole
1173    /// licence for skipping the read: the digest is not being trusted about the
1174    /// file's identity, only reused instead of recomputed from bytes already
1175    /// proven unchanged.
1176    ///
1177    /// `None` means the table could not be honoured — most often the artifact was
1178    /// evicted from a shared cache directory between spawns. The caller then reads
1179    /// and compiles as usual, which is correct, just slower.
1180    fn linked_module_artifact(
1181        &self,
1182        file_path: &Path,
1183        canonical: &Path,
1184        content_hash: [u8; 32],
1185        compilation_context: &crate::module_artifact::ModuleCompilationContext,
1186    ) -> Option<Arc<PreparedModuleArtifact>> {
1187        if !bytecode_cache::cache_enabled() {
1188            return None;
1189        }
1190        if let Some(prepared) = self.prepared_module_cache.get_with_context(
1191            canonical,
1192            content_hash,
1193            ModuleProvenance::User,
1194            compilation_context,
1195        ) {
1196            return Some(prepared);
1197        }
1198        // `User` matches the in-memory probe just above: the link table is
1199        // an ordinary-VM fast path, and `enable_trusted_host_dispatch` clears
1200        // it outright, so a trusted graph never reaches here.
1201        let key = bytecode_cache::CacheKey::from_module_content_hash(
1202            content_hash,
1203            compilation_context,
1204            ModuleProvenance::User,
1205        );
1206        let artifact = bytecode_cache::load_module_for_key(file_path, key).artifact?;
1207        Some(self.prepared_module_cache.insert_with_context(
1208            canonical.to_path_buf(),
1209            content_hash,
1210            compilation_context,
1211            Arc::new(PreparedModuleArtifact::from_cached(artifact)),
1212        ))
1213    }
1214
1215    /// Bind imports that were deferred because their target module was still
1216    /// mid-load (an import cycle). By the time the import stack has unwound,
1217    /// both the importing and target modules are fully instantiated and cached,
1218    /// so we can resolve the requested names against the target and define them
1219    /// into the importer's shared, mutable `module_state`. That env is the one
1220    /// every closure from the importing module consults (after its local env)
1221    /// at call time, so the late binding becomes visible without needing to
1222    /// rewrite the closures' captured lexical snapshots.
1223    fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
1224        if self.deferred_cyclic_imports.is_empty() {
1225            return Ok(());
1226        }
1227        let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
1228        let mut still_pending = Vec::new();
1229        for import in deferred {
1230            let (Some(importer), Some(target)) = (
1231                self.module_cache.get(&import.importer).cloned(),
1232                self.module_cache.get(&import.target).cloned(),
1233            ) else {
1234                // One endpoint is not cached yet (a lazy import inside a
1235                // function body can defer before the other side loads). Keep
1236                // it for a later flush.
1237                still_pending.push(import);
1238                continue;
1239            };
1240
1241            let mut module_state = importer._module_state.lock();
1242            if let Some(alias) = &import.namespace_alias {
1243                if module_state.get(alias).is_none() {
1244                    let dict = build_namespace_dict(
1245                        &import.target.display().to_string(),
1246                        &target,
1247                        import.namespace_members.as_deref(),
1248                    )?;
1249                    module_state.define(alias, dict, false)?;
1250                }
1251                continue;
1252            }
1253
1254            let export_names = module_import_names(
1255                &import.target.display().to_string(),
1256                &target,
1257                import.selected_names.as_deref(),
1258                ImportNameUse::Binding,
1259            )?;
1260
1261            for name in export_names {
1262                // A real local declaration (or an already-bound non-cyclic
1263                // import) wins over the cyclic re-binding.
1264                if module_state.get(&name).is_some() {
1265                    continue;
1266                }
1267                if let Some(closure) = target.functions.get(&name) {
1268                    module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
1269                } else if let Some(value) = target.public_values.get(&name) {
1270                    // Init-backed public declarations imported across a cycle.
1271                    module_state.define(&name, value.clone(), false)?;
1272                } else if target
1273                    .public_exports
1274                    .get(&name)
1275                    .is_some_and(|kind| !kind.has_runtime_value())
1276                {
1277                    // Type-only public declarations carry no runtime binding.
1278                    continue;
1279                } else {
1280                    return Err(VmError::Runtime(format!(
1281                        "Import error: '{name}' is not defined in {}",
1282                        import.target.display()
1283                    )));
1284                }
1285            }
1286        }
1287        self.deferred_cyclic_imports = still_pending;
1288        Ok(())
1289    }
1290
1291    /// Return the path key that `execute_import` would use to cache the
1292    /// LoadedModule for this import string. Used by the re-export pass to
1293    /// look up the already-loaded source module after `execute_import`
1294    /// has populated [`Vm::module_cache`].
1295    fn cache_key_for_import(&self, path: &str) -> Result<PathBuf, VmError> {
1296        if let Some(module) = path
1297            .strip_prefix("std/")
1298            .or_else(|| (path == "observability").then_some("observability"))
1299        {
1300            return Ok(PathBuf::from(format!("<stdlib>/{module}.harn")));
1301        }
1302        let base = self
1303            .source_dir
1304            .clone()
1305            .unwrap_or_else(|| PathBuf::from("."));
1306        let file_path = self.resolve_module_import_path(&base, path)?;
1307        Ok(file_path.canonicalize().unwrap_or(file_path))
1308    }
1309
1310    async fn loaded_module_for_path(
1311        &mut self,
1312        path: &Path,
1313    ) -> Result<(PathBuf, Arc<LoadedModule>), VmError> {
1314        self.ensure_execution_available()?;
1315        let path_str = path.to_string_lossy().into_owned();
1316        self.materialize_import(&path_str).await?;
1317
1318        let mut file_path = if path.is_absolute() {
1319            path.to_path_buf()
1320        } else {
1321            self.source_dir
1322                .clone()
1323                .unwrap_or_else(|| PathBuf::from("."))
1324                .join(path)
1325        };
1326        if !file_path.exists() && file_path.extension().is_none() {
1327            file_path.set_extension("harn");
1328        }
1329
1330        let canonical = file_path
1331            .canonicalize()
1332            .unwrap_or_else(|_| file_path.clone());
1333        let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
1334            VmError::Runtime(format!(
1335                "Import error: failed to cache loaded module '{}'",
1336                canonical.display()
1337            ))
1338        })?;
1339        Ok((canonical, loaded))
1340    }
1341
1342    /// Load one explicitly public callable from a module.
1343    pub async fn load_public_module_callable(
1344        &mut self,
1345        path: &Path,
1346        name: &str,
1347    ) -> Result<Arc<VmClosure>, VmError> {
1348        let (canonical, loaded) = self.loaded_module_for_path(path).await?;
1349        if !loaded.public_exports.contains_key(name) {
1350            let hint = if loaded.functions.contains_key(name) {
1351                "; it is defined there but not `pub`"
1352            } else {
1353                ""
1354            };
1355            return Err(VmError::Runtime(format!(
1356                "callable '{name}' is not exported by module '{}'{hint}",
1357                canonical.display()
1358            )));
1359        }
1360        loaded.functions.get(name).cloned().ok_or_else(|| {
1361            VmError::Runtime(format!(
1362                "Import error: exported callable '{name}' is missing from {}",
1363                canonical.display()
1364            ))
1365        })
1366    }
1367
1368    /// Load a module file and return the exported function closures that
1369    /// would be visible to a wildcard import.
1370    pub async fn load_module_exports(
1371        &mut self,
1372        path: &Path,
1373    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1374        let (canonical, loaded) = self.loaded_module_for_path(path).await?;
1375        exported_function_closures(&loaded, &canonical)
1376    }
1377
1378    /// Load synthetic source keyed by a synthetic module path and return
1379    /// the exported function closures that a wildcard import would expose.
1380    pub async fn load_module_exports_from_source(
1381        &mut self,
1382        source_key: impl Into<PathBuf>,
1383        source: &str,
1384    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1385        self.ensure_execution_available()?;
1386        let synthetic = source_key.into();
1387        let loaded = self
1388            .load_module_from_source(synthetic.clone(), source)
1389            .await?;
1390        exported_function_closures(&loaded, &synthetic)
1391    }
1392
1393    /// Load exports from a source snapshot whose complete import generation
1394    /// was installed through [`Vm::set_prepared_module_generation`]. The root
1395    /// artifact and its imports are instantiated from that immutable
1396    /// generation while runtime state remains local to this VM.
1397    pub async fn load_prepared_module_exports_from_source(
1398        &mut self,
1399        source_key: impl Into<PathBuf>,
1400        source: &str,
1401    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1402        self.ensure_execution_available()?;
1403        let synthetic = source_key.into();
1404        let loaded = self
1405            .load_module_from_source_with_preparation(synthetic.clone(), source, true)
1406            .await?;
1407        exported_function_closures(&loaded, &synthetic)
1408    }
1409
1410    /// Load one callable from synthetic source for a host dispatch surface
1411    /// that has already selected the callable through its own policy. This is
1412    /// deliberately separate from module exports: script imports must
1413    /// continue to see only declarations in the typed public export contract.
1414    pub async fn load_module_callable_from_source(
1415        &mut self,
1416        source_key: impl Into<PathBuf>,
1417        source: &str,
1418        name: &str,
1419    ) -> Result<Option<Arc<VmClosure>>, VmError> {
1420        self.ensure_execution_available()?;
1421        let synthetic = source_key.into();
1422        let loaded = self.load_module_from_source(synthetic, source).await?;
1423        Ok(loaded.functions.get(name).cloned())
1424    }
1425
1426    /// Load a module by import path (`std/foo`, relative module path, or
1427    /// package import) and return the exported function closures that a
1428    /// wildcard import would expose.
1429    pub async fn load_module_exports_from_import(
1430        &mut self,
1431        import_path: &str,
1432    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1433        self.ensure_execution_available()?;
1434        self.materialize_import(import_path).await?;
1435
1436        if let Some(module) = import_path
1437            .strip_prefix("std/")
1438            .or_else(|| (import_path == "observability").then_some("observability"))
1439        {
1440            let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
1441            let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
1442                VmError::Runtime(format!(
1443                    "Import error: failed to cache loaded module '{}'",
1444                    synthetic.display()
1445                ))
1446            })?;
1447            return exported_function_closures(&loaded, &synthetic);
1448        }
1449
1450        let base = self
1451            .source_dir
1452            .clone()
1453            .unwrap_or_else(|| PathBuf::from("."));
1454        let file_path = self.resolve_module_import_path(&base, import_path)?;
1455        self.load_module_exports(&file_path).await
1456    }
1457}
1458
1459#[cfg(test)]
1460#[path = "modules_tests.rs"]
1461mod tests;