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