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