Skip to main content

harn_vm/vm/
modules.rs

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