Skip to main content

harn_vm/vm/
modules.rs

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