Skip to main content

harn_vm/vm/
modules.rs

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