Skip to main content

harn_vm/vm/
modules.rs

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